Skip to content

Commit

Permalink
Merge pull request #276 from jrhender/miniflare-jest-env
Browse files Browse the repository at this point in the history
Initial test around stampy.ts
  • Loading branch information
Aprillion authored Oct 7, 2023
2 parents 166b4ee + 30845a9 commit 73f96b1
Show file tree
Hide file tree
Showing 11 changed files with 118,436 additions and 42 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ on:
jobs:
pr-check:
runs-on: ubuntu-latest
env:
CODA_TOKEN: ${{ secrets.CODA_TOKEN }}
steps:
- uses: actions/checkout@v3
- name: lint
run: |
npm ci
npm run lint
npm run build
cp wrangler.toml.template wrangler.toml
npm run refresh-test-data
npm run test
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ $ npm run lint
$ npm run test
```

Tests may rely on cached data from coda.
This data can be refreshed by running:

```sh
$ npm run refresh-test-data
```

## Deployment to your CF Worker

```sh
Expand Down
117,380 changes: 117,380 additions & 0 deletions app/mocks/coda-responses/cached-coda-responses.json

Large diffs are not rendered by default.

128 changes: 128 additions & 0 deletions app/mocks/coda-responses/refresh-coda-data-for-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* This is a script to refresh the mock return data for tests.
*/

import * as fs from 'fs'
import _ from 'lodash'
import * as path from 'path'
import * as toml from 'toml'
import {
ALL_ANSWERS_TABLE,
BANNERS_TABLE,
QUESTION_DETAILS_TABLE,
TAGS_TABLE,
makeCodaRequest,
} from '../../server-utils/coda-urls'

async function main(): Promise<void> {
const codaRequestsToCache: CodaRequestParams[] = [
{
table: QUESTION_DETAILS_TABLE,
queryColumn: 'UI ID',
queryValue: '0',
},
{
table: ALL_ANSWERS_TABLE,
},
{
table: TAGS_TABLE,
queryColumn: 'Internal?',
queryValue: 'false',
},
{
table: BANNERS_TABLE,
},
]

const cacheResults: CachedCodaQueries = await Promise.all(
codaRequestsToCache.map(async (codaParams) => {
const cachedRequests = await getCodaData(codaParams)
return {codaParams, cachedRequests}
})
)
const writeData = JSON.stringify(cacheResults, null, 2)
const filename = `cached-coda-responses.json`
await writeFile(writeData, filename)
}

export type CachedCodaQueries = Array<CachedCodaQuery>
type CachedCodaQuery = {
codaParams: CodaRequestParams
cachedRequests: CachedRequest[]
}
type CodaRequestParams = {
table: string
queryColumn?: string
queryValue?: string
}
type CachedRequest = {
url: string
httpMethod: string
responseData: any
}

const getCodaData = async (codaRequestParams: CodaRequestParams) => {
const codaUrl = makeCodaRequest(codaRequestParams)
console.log(`Fetching ${codaUrl}`)
const cachedRequests: CachedRequest[] = []
await paginatedGet(codaUrl, cachedRequests)
return cachedRequests
}

const paginatedGet = async (url: string, responses: any[]) => {
const responseData = await getData(url)
const data = {url, httpMethod: 'GET', responseData}
responses.push(data)

if (responseData.nextPageLink) {
await paginatedGet(responseData.nextPageLink, responses)
}
return
}

const readCodaToken = (): string => {
const tokenFromEnv = process.env.CODA_TOKEN
if (tokenFromEnv) {
console.log('found Coda token from environment')
return tokenFromEnv
}
const wranglerToml = fs.readFileSync('wrangler.toml', 'utf8')
const config = toml.parse(wranglerToml)
const tokenFromToml: string = config.vars.CODA_TOKEN
if (tokenFromToml) {
console.log('found Coda token from wrangler config')
return tokenFromToml
}

throw Error('unable to get a Coda token')
}

const getDataWithAuthz = async (url: string, authzToken: string) => {
const options = {
headers: {
Authorization: `Bearer ${authzToken}`,
},
}
const response = await fetch(url, options)
return await response.json()
}

const getData = _.partialRight(getDataWithAuthz, readCodaToken())

const writeFile = (data: string, filename: string): Promise<void> => {
const filePath = path.join(__dirname, filename)

return new Promise((resolve, reject) => {
fs.writeFile(filePath, data, (err) => {
if (err) {
console.error('An error occurred:', err)
reject(err)
} else {
console.log('File has been written successfully.')
resolve()
}
})
})
}

main()
36 changes: 36 additions & 0 deletions app/server-utils/coda-urls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* This file contains the URLs for the Coda API.
* It is a separate file in part so usage doesn't require CF worker env vars.
*/

// Use table ide, rather than names, in case of renames
export const QUESTION_DETAILS_TABLE = 'grid-sync-1059-File' // Answers
export const INITIAL_QUESTIONS_TABLE = 'table-yWog6qRzV4' // Initial questions
export const ON_SITE_TABLE = 'table-aOTSHIz_mN' // On-site answers
export const ALL_ANSWERS_TABLE = 'table-YvPEyAXl8a' // All answers
export const INCOMING_QUESTIONS_TABLE = 'grid-S_6SYj6Tjm' // Incoming questions
export const TAGS_TABLE = 'grid-4uOTjz1Rkz'
export const WRITES_TABLE = 'table-eEhx2YPsBE'
export const GLOSSARY_TABLE = 'grid-_pSzs23jmw'
export const BANNERS_TABLE = 'grid-3WgZ9_NkvO'

type CodaRequest = {
table: string
queryColumn?: string
queryValue?: string
limit?: number
}
export const makeCodaRequest = ({table, queryColumn, queryValue, limit}: CodaRequest): string => {
let params = `useColumnNames=true&sortBy=natural&valueFormat=rich${
queryColumn && queryValue ? `&query=${quote(queryColumn)}:${quote(queryValue)}` : ''
}`
if (limit) {
params = `${params}&limit=${limit}`
}
return `https://coda.io/apis/v1/docs/${CODA_DOC_ID}/tables/${enc(table)}/rows?${params}`
}

export const CODA_DOC_ID = 'fau7sl2hmG'

const enc = encodeURIComponent
const quote = (x: string) => encodeURIComponent(`"${x.replace(/"/g, '\\"')}"`)
54 changes: 54 additions & 0 deletions app/server-utils/stampy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {loadQuestionDetail} from '~/server-utils/stampy'
import cachedCodaQueriesJson from '~/mocks/coda-responses/cached-coda-responses.json'
import {QUESTION_DETAILS_TABLE} from './coda-urls'
import {CachedCodaQueries} from '~/mocks/coda-responses/refresh-coda-data-for-tests'
import _ from 'lodash'

export const questions: Array<string> = ['0']
const cachedQueries = cachedCodaQueriesJson as CachedCodaQueries

describe('loadQuestionDetail', () => {
it.each<string>(questions)('can load question %i', async (questionId) => {
const fetchMock = getMiniflareFetchMock()
fetchMock.disableNetConnect()
const mockCodaRequest = _.partial(mockCodaRequestUsingMiniflareFetchMock, fetchMock)
const allCachedRequests = cachedQueries.flatMap((codaQuery) => codaQuery.cachedRequests)
for (const cachedRequest of allCachedRequests) {
mockCodaRequest(cachedRequest.url, cachedRequest.responseData)
}

const cachedQuery = cachedQueries.find((cachedQuery) =>
_.isEqual(cachedQuery.codaParams, {
table: QUESTION_DETAILS_TABLE,
queryColumn: 'UI ID',
queryValue: questionId,
})
)
if (!cachedQuery) {
throw new Error(`Cached response not found for question ${questionId}`)
}

const questionDetail = await loadQuestionDetail('NEVER_RELOAD', questionId)

const cachedQuestionData = cachedQuery.cachedRequests[0].responseData
const firstItem = cachedQuestionData.items[0]
expect(questionDetail.data.status).toBe(firstItem.values.Status.name)
const linkUrl = new URL(firstItem.values.Link.url)
expect(questionDetail.data.answerEditLink).toBe(linkUrl.origin + linkUrl.pathname)
})
})

const mockCodaRequestUsingMiniflareFetchMock = async (
fetchMock: ReturnType<typeof getMiniflareFetchMock>,
urlString: string,
responseData: any
) => {
const url = new URL(urlString)
const origin = fetchMock.get(`${url.origin}`)
origin
.intercept({
method: 'GET',
path: url.href.replace(url.origin, ''),
})
.reply(200, responseData)
}
47 changes: 15 additions & 32 deletions app/server-utils/stampy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ import {
urlToIframe,
convertToHtmlAndWrapInDetails,
} from '~/server-utils/parsing-utils'
import {
ALL_ANSWERS_TABLE,
CODA_DOC_ID,
GLOSSARY_TABLE,
INCOMING_QUESTIONS_TABLE,
INITIAL_QUESTIONS_TABLE,
ON_SITE_TABLE,
QUESTION_DETAILS_TABLE,
TAGS_TABLE,
WRITES_TABLE,
BANNERS_TABLE,
makeCodaRequest,
} from './coda-urls'

export enum QuestionState {
OPEN = '_',
Expand Down Expand Up @@ -88,7 +101,7 @@ type CodaRowCommon = {
browserLink: string
}

type AnswersRow = CodaRowCommon & {
export type AnswersRow = CodaRowCommon & {
values: {
'Edit Answer': string
Link: {
Expand Down Expand Up @@ -132,26 +145,12 @@ type BannersRow = CodaRowCommon & {
}
}
type CodaRow = AnswersRow | TagsRow | GlossaryRow | BannersRow
type CodaResponse = {
export type CodaResponse = {
items: CodaRow[]
nextPageLink: string | null
}

const CODA_DOC_ID = 'fau7sl2hmG'

// Use table ide, rather than names, in case of renames
const QUESTION_DETAILS_TABLE = 'grid-sync-1059-File' // Answers
const INITIAL_QUESTIONS_TABLE = 'table-yWog6qRzV4' // Initial questions
const ON_SITE_TABLE = 'table-aOTSHIz_mN' // On-site answers
const ALL_ANSWERS_TABLE = 'table-YvPEyAXl8a' // All answers
const INCOMING_QUESTIONS_TABLE = 'grid-S_6SYj6Tjm' // Incoming questions
const TAGS_TABLE = 'grid-4uOTjz1Rkz'
const WRITES_TABLE = 'table-eEhx2YPsBE'
const GLOSSARY_TABLE = 'grid-_pSzs23jmw'
const BANNERS_TABLE = 'grid-3WgZ9_NkvO'

const enc = encodeURIComponent
const quote = (x: string) => encodeURIComponent(`"${x.replace(/"/g, '\\"')}"`)
let allTags = {} as Record<string, Tag>
let allBanners = {} as Record<string, Banner>

Expand Down Expand Up @@ -213,22 +212,6 @@ const paginateCoda = async (url: string): Promise<CodaRow[]> => {
return items
}

type CodaRequest = {
table: string
queryColumn?: string
queryValue?: string
limit?: number
}
const makeCodaRequest = ({table, queryColumn, queryValue, limit}: CodaRequest): string => {
let params = `useColumnNames=true&sortBy=natural&valueFormat=rich${
queryColumn && queryValue ? `&query=${quote(queryColumn)}:${quote(queryValue)}` : ''
}`
if (limit) {
params = `${params}&limit=${limit}`
}
return `https://coda.io/apis/v1/docs/${CODA_DOC_ID}/tables/${enc(table)}/rows?${params}`
}

const getCodaRows = async (
table: string,
queryColumn?: string,
Expand Down
5 changes: 4 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testEnvironment: 'miniflare',
moduleNameMapper: {
'^~/(.*)$': '<rootDir>/app/$1',
},
}
Loading

0 comments on commit 73f96b1

Please sign in to comment.