Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/producthunt

src/smoke-test.ts

115 lines · 3.7 KB · TypeScript
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'),
	})
}