import { accounts } from './accounts.ts'
import { parseAuthInput } from './auth.ts'
import { getViewer, listTodayPosts } from './domain.ts'
import { inferOperation, isReadOnlyQuery, oauthConnectUrl } from './setup.ts'
import { todayBounds } from './today.ts'
import type { ProductHuntAuthInput } from './types.ts'
import { optionalBoolean, requireRecord } from './types.ts'
export type SmokeTestInput = ProductHuntAuthInput & {
/** When true, skip live Product Hunt calls even if credentials exist. */
setupOnly?: boolean
}
function selfCheck() {
const bounds = todayBounds('America/Los_Angeles', new Date('2026-08-22T18:00:00.000Z'))
const connect = oauthConnectUrl('producthunt')
return {
ok:
bounds.day === '2026-08-22' &&
Date.parse(bounds.postedAfter) < Date.parse(bounds.postedBefore) &&
isReadOnlyQuery('query { posts { edges { node { id } } } }') &&
!isReadOnlyQuery('mutation { userFollow(input: { userId: "x" }) { user { id } } }') &&
inferOperation('mutation Foo { goalCreate(input: { title: "x" }) { goal { id } } }') ===
'mutation' &&
connect.includes('provider=producthunt') &&
connect.includes('scopes=public+private'),
todayWindowValid: Date.parse(bounds.postedAfter) < Date.parse(bounds.postedBefore),
readOnlyQueriesSkipConfirm: isReadOnlyQuery('query { viewer { user { id } } }'),
mutationsNeedConfirm: !isReadOnlyQuery('mutation { userFollowUndo(input: { userId: "x" }) { user { id } } }'),
connectUrlHasPublicPrivate: connect.includes('scopes=public+private'),
}
}
/**
* Verify Product Hunt helpers. Without credentials this still returns setup
* URLs. With credentials it reads today's featured hunts and does not return
* hunter PII.
*/
export async function smokeTest(params: SmokeTestInput = {}) {
const checks = selfCheck()
const info = await accounts(params)
const canLive = Boolean(info.preferredAuth) && params.setupOnly !== true
if (!canLive) {
return {
ok: checks.ok,
live: false,
selfCheck: checks,
authMode: info.preferredAuth,
integrationName: info.integrationName,
secretName: info.secretName,
oauthConnected: info.oauthConnected,
tokenSaved: info.tokenSaved,
hasViewer: false,
todayPostCount: 0,
featuredPost: null as { slug: string; name: string } | null,
setup: {
connectUrl: info.connectUrl,
accessTokenUrl: info.accessTokenUrl,
clientSecretUrl: info.clientSecretUrl,
callbackUrl: info.callbackUrl,
requiredHost: info.requiredHost,
},
}
}
const today = await listTodayPosts({ ...params, first: 1 })
let hasViewer = false
try {
const viewer = await getViewer(params)
hasViewer = Boolean(viewer?.id)
} catch {
hasViewer = false
}
const featured = today.nodes[0]
return {
ok: true,
live: true,
selfCheck: checks,
authMode: info.preferredAuth,
integrationName: info.integrationName,
secretName: info.secretName,
oauthConnected: info.oauthConnected,
tokenSaved: info.tokenSaved,
hasViewer,
todayPostCount: today.nodes.length,
day: today.day,
timeZone: today.timeZone,
featuredPost: featured ? { slug: featured.slug, name: featured.name } : null,
setup: {
connectUrl: info.connectUrl,
accessTokenUrl: info.accessTokenUrl,
clientSecretUrl: info.clientSecretUrl,
callbackUrl: info.callbackUrl,
requiredHost: info.requiredHost,
},
}
}
/**
* Verify Product Hunt helpers and return setup URLs plus an optional live sample.
* @example
* import smokeTest from 'kody:@kody/producthunt/smoke-test'
* const result = await smokeTest({ setupOnly: true })
*/
export default async function smokeTestEntrypoint(
params: SmokeTestInput & Record<string, unknown> = {},
) {
const input = requireRecord(params, 'smoke-test')
return smokeTest({
...parseAuthInput(input),
setupOnly: optionalBoolean(input.setupOnly, 'setupOnly'),
})
}