Skip to content

Commit

Permalink
couple more cute little tests
Browse files Browse the repository at this point in the history
  • Loading branch information
joelhooks committed Mar 25, 2024
1 parent 8281104 commit 5d8f48c
Show file tree
Hide file tree
Showing 9 changed files with 250 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ export const courseBuilderConfig: CourseBuilderConfig = {

export const {
handlers: { POST, GET },
coursebuilder,
} = NextCourseBuilder(courseBuilderConfig)
4 changes: 2 additions & 2 deletions packages/core/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ export async function CourseBuilderInternal(
isPost: method === 'POST',
})

console.log('options', options)

if (method === 'GET') {
switch (action) {
case 'srt':
return await actions.srt(request, cookies, options)
case 'session':
return {}
}
} else {
switch (action) {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/lib/utils/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function parseActionAndProviderId(
if (!isCourseBuilderAction(action))
throw new UnknownAction(`Cannot parse action at ${pathname}`)

if (providerId && !['webhook', 'srt'].includes(action))
if (providerId && !['webhook', 'srt', 'session'].includes(action))
throw new UnknownAction(`Cannot parse action at ${pathname}`)

return { action, providerId }
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export interface CookieOption {
options: CookieSerializeOptions
}

export type CourseBuilderAction = 'webhook' | 'srt'
export type CourseBuilderAction = 'webhook' | 'srt' | 'session'

export interface RequestInternal {
url: URL
Expand Down
34 changes: 23 additions & 11 deletions packages/core/test/env.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import {
afterAll,
afterEach,
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest'

import { CourseBuilderConfig, setEnvDefaults } from '../src/index.js'
import { beforeEach, describe, expect, it } from 'vitest'

import { CourseBuilderConfig, setEnvDefaults } from '../src'
import Deepgram from '../src/providers/deepgram.js'

const testConfig: CourseBuilderConfig = {
Expand All @@ -31,4 +23,24 @@ describe('config is inferred from environment variables', () => {
// @ts-expect-error
expect(p1.apiKey).toBe(env.COURSEBUILDER_DEEPGRAM_API_KEY)
})

it('COURSEBUILDER_URL', () => {
const env = { COURSEBUILDER_URL: 'http://n/api/coursebuilder' }
setEnvDefaults(env, courseBuilderConfig)
expect(courseBuilderConfig.basePath).toBe('/api/coursebuilder')
})

it('COURSEBUILDER_URL + prefer config', () => {
const env = { COURSEBUILDER_URL: 'http://n/api/coursebuilder' }
const fromConfig = '/basepath-from-config'
courseBuilderConfig.basePath = fromConfig
setEnvDefaults(env, courseBuilderConfig)
expect(courseBuilderConfig.basePath).toBe(fromConfig)
})

it('COURSEBUILDER_URL, but invalid value', () => {
const env = { COURSEBUILDER_URL: 'secret' }
setEnvDefaults(env, courseBuilderConfig)
expect(courseBuilderConfig.basePath).toBe('/coursebuilder')
})
})
24 changes: 21 additions & 3 deletions packages/next/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,31 @@ import { NextRequest } from 'next/server'

import { CourseBuilder } from '@coursebuilder/core'

import { NextCourseBuilderConfig } from './lib'
import { initCourseBuilder, NextCourseBuilderConfig } from './lib'
import { reqWithEnvURL, setEnvDefaults } from './lib/env'

export default function NextCourseBuilder(
config: NextCourseBuilderConfig,
config:
| NextCourseBuilderConfig
| ((request: NextRequest | undefined) => NextCourseBuilderConfig),
): NextCourseBuilderResult {
const httpHandler = (req: NextRequest) => CourseBuilder(req, config)
if (typeof config === 'function') {
const httpHandler = (req: NextRequest) => {
const _config = config(req)
setEnvDefaults(_config)
return CourseBuilder(reqWithEnvURL(req), _config)
}
return {
handlers: { GET: httpHandler, POST: httpHandler } as const,
coursebuilder: initCourseBuilder(config, (c) => setEnvDefaults(c)),
}
}
setEnvDefaults(config)
const httpHandler = (req: NextRequest) =>
CourseBuilder(reqWithEnvURL(req), config)
return {
handlers: { POST: httpHandler, GET: httpHandler } as const,
coursebuilder: initCourseBuilder(config, (c) => setEnvDefaults(c)),
}
}

Expand All @@ -22,4 +39,5 @@ type AppRouteHandlers = Record<

export interface NextCourseBuilderResult {
handlers: AppRouteHandlers
coursebuilder: any
}
28 changes: 28 additions & 0 deletions packages/next/src/lib/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { NextRequest } from 'next/server'

import {
setEnvDefaults as coreSetEnvDefaults,
CourseBuilderConfig,
} from '@coursebuilder/core'

export function reqWithEnvURL(req: NextRequest): NextRequest {
const url = process.env.COURSEBUILDER_URL
if (!url) return req
const { origin: envOrigin } = new URL(url)
const { href, origin } = req.nextUrl
return new NextRequest(href.replace(origin, envOrigin), req)
}

export function setEnvDefaults(config: CourseBuilderConfig) {
try {
const url = process.env.COURSEBUILDER_URL
if (!url) return
const { pathname } = new URL(url)
if (pathname === '/') return
config.basePath ||= pathname
} catch {
} finally {
config.basePath ||= '/api/coursebuilder'
coreSetEnvDefaults(process.env, config)
}
}
150 changes: 149 additions & 1 deletion packages/next/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,151 @@
import { CourseBuilderConfig } from '@coursebuilder/core'
import {
GetServerSidePropsContext,
NextApiRequest,
NextApiResponse,
} from 'next'
import { headers } from 'next/headers'
import { NextMiddleware, NextRequest, NextResponse } from 'next/server'

import {
CourseBuilder,
CourseBuilderConfig,
createActionURL,
} from '@coursebuilder/core'

import { reqWithEnvURL } from './env'
import { AppRouteHandlerFn } from './types'

export interface NextCourseBuilderConfig extends CourseBuilderConfig {}

export type WithCourseBuilderArgs =
| [NextCourseBuilderRequest, any]
| [AppRouteHandlerFn]
| [NextApiRequest, NextApiResponse]
| [GetServerSidePropsContext]
| []

function isReqWrapper(arg: any): arg is AppRouteHandlerFn {
return typeof arg === 'function'
}

export function initCourseBuilder(
config:
| NextCourseBuilderConfig
| ((request: NextRequest | undefined) => NextCourseBuilderConfig),
onLazyLoad?: (config: NextCourseBuilderConfig) => void,
) {
if (typeof config === 'function') {
return (...args: WithCourseBuilderArgs) => {
if (!args.length) {
// React Server Components
const _headers = headers()
const _config = config(undefined) // Review: Should we pass headers() here instead?
onLazyLoad?.(_config)

return getSession(_headers, _config).then((r) => r.json())
}

if (args[0] instanceof Request) {
// middleware.ts inline
// export { auth as default } from "auth"
const req = args[0]
const ev = args[1]
const _config = config(req)
onLazyLoad?.(_config)

// args[0] is supposed to be NextRequest but the instanceof check is failing.
return handleCourseBuilder([req, ev], _config)
}

if (isReqWrapper(args[0])) {
// middleware.ts wrapper/route.ts
// import { auth } from "auth"
// export default auth((req) => { console.log(req.auth) }})
const userMiddlewareOrRoute = args[0]
return async (...args: Parameters<AppRouteHandlerFn>) => {
return handleCourseBuilder(
args,
config(args[0]),
userMiddlewareOrRoute,
)
}
}
// API Routes, getServerSideProps
const request = 'req' in args[0] ? args[0].req : args[0]
const response: any = 'res' in args[0] ? args[0].res : args[1]
// @ts-expect-error -- request is NextRequest
const _config = config(request)
onLazyLoad?.(_config)

// @ts-expect-error -- request is NextRequest
return getSession(new Headers(request.headers), _config).then(
async (authResponse) => {
const auth = await authResponse.json()

for (const cookie of authResponse.headers.getSetCookie())
if ('headers' in response)
response.headers.append('set-cookie', cookie)
else response.appendHeader('set-cookie', cookie)

return auth satisfies any | null
},
)
}
}
return (...args: WithCourseBuilderArgs) => {
if (!args.length) {
const _headers = headers()
return getSession(_headers, config)
}

if (args[0] instanceof Request) {
// middleware.ts inline
// export { auth as default } from "auth"
const req = args[0]
const ev = args[1]
return handleCourseBuilder([req, ev], config)
}
}
}

async function getSession(headers: Headers, config: CourseBuilderConfig) {
// TODO what's a coursebuilder session?

const url = createActionURL(
'session',
// @ts-expect-error `x-forwarded-proto` is not nullable, next.js sets it by default
headers.get('x-forwarded-proto'),
headers,
process.env,
config.basePath,
)

const request = new Request(url, {
headers: { cookie: headers.get('cookie') ?? '' },
})

return CourseBuilder(request, config)
}

async function handleCourseBuilder(
args: Parameters<NextMiddleware | AppRouteHandlerFn>,
config: CourseBuilderConfig,
userRoute?: AppRouteHandlerFn,
) {
const request = reqWithEnvURL(args[0])
const sessionResponse = await getSession(request.headers, config)
const coursebuilder = await sessionResponse.json()

let response: any = NextResponse.next?.()

const finalResponse = new Response(response?.body, response)

for (const cookie of sessionResponse.headers.getSetCookie())
finalResponse.headers.append('set-cookie', cookie)

return finalResponse
}

export interface NextCourseBuilderRequest extends NextRequest {
coursebuilder: any | null
}
24 changes: 24 additions & 0 deletions packages/next/src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { NextRequest } from 'next/server'

/**
* AppRouteHandlerFnContext is the context that is passed to the handler as the
* second argument.
*/
type AppRouteHandlerFnContext = {
params?: Record<string, string | string[]>
}
/**
* Handler function for app routes. If a non-Response value is returned, an error
* will be thrown.
*/
export type AppRouteHandlerFn = (
/**
* Incoming request object.
*/
req: NextRequest,
/**
* Context properties on the request (including the parameters if this was a
* dynamic route).
*/
ctx: AppRouteHandlerFnContext,
) => void | Response | Promise<void | Response>

0 comments on commit 5d8f48c

Please sign in to comment.