mirror of
https://github.com/ThatGuySam/doesitarm.git
synced 2026-05-18 06:44:46 -07:00
Finish axios migration via shared native HTTP helper
Replace all in-scope axios callsites with a new helpers/http.js wrapper over native fetch, including JSON/text GET, JSON POST, HEAD checks, and transient 5xx retry behavior; update all browser, build, script, and proxy API clients to use it; add focused unit tests; and remove axios from package dependencies. Constraint: Preserve API/build and deployment behavior while lowering transport surface area. Rejected: inline fetch replacements at each callsite | rejected to avoid inconsistent error/retry semantics. Confidence: high Scope-risk: moderate Directive: Keep helper in place as the transport boundary and update tests when changing request semantics. Tested: pnpm run -s typecheck, pnpm -s run test-prebuild, pnpm -s run test, pnpm -s run test:browser, pnpm -s run netlify-build, smoke GETs on /apple-silicon-app-test and /apple-silicon-app-test/?version=2 Not-tested: branch/netlify deployment health in CI pipeline after merge
This commit is contained in:
parent
d39a2a1d6c
commit
d45b587434
25 changed files with 824 additions and 267 deletions
|
|
@ -1,7 +1,6 @@
|
|||
import fs from 'fs-extra'
|
||||
import has from 'just-has'
|
||||
import { test, expect, beforeAll } from 'vitest'
|
||||
import axios from 'axios'
|
||||
import { JSDOM } from 'jsdom'
|
||||
import { structuredDataTestHtml } from 'structured-data-testing-tool'
|
||||
import { Google } from 'structured-data-testing-tool/presets'
|
||||
|
|
@ -11,6 +10,7 @@ import {
|
|||
getVideoImages,
|
||||
ListingDetails
|
||||
} from '~/helpers/listing-page.js'
|
||||
import { getJson } from '~/helpers/http.js'
|
||||
import { headPropertyTypes } from '~/test/helpers/head.js'
|
||||
import { PageHead } from '~/helpers/config-node.js'
|
||||
|
||||
|
|
@ -73,8 +73,7 @@ beforeAll(async () => {
|
|||
continue
|
||||
}
|
||||
|
||||
const { data } = await axios.get(`${process.env.PUBLIC_API_DOMAIN}${apiPath}`)
|
||||
context.listings[caseEndpoint] = data
|
||||
context.listings[caseEndpoint] = await getJson( `${process.env.PUBLIC_API_DOMAIN}${apiPath}` )
|
||||
}
|
||||
|
||||
// Initialize listing details
|
||||
|
|
|
|||
170
test/prebuild/http.test.ts
Normal file
170
test/prebuild/http.test.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest'
|
||||
|
||||
import {
|
||||
getJson,
|
||||
getText,
|
||||
headOk,
|
||||
postJson,
|
||||
requestJson,
|
||||
shouldRetryError
|
||||
} from '~/helpers/http.js'
|
||||
|
||||
describe( 'http helper', () => {
|
||||
const fetchMock = vi.fn()
|
||||
|
||||
beforeEach( () => {
|
||||
vi.stubGlobal( 'fetch', fetchMock )
|
||||
} )
|
||||
|
||||
afterEach( () => {
|
||||
vi.unstubAllGlobals()
|
||||
vi.restoreAllMocks()
|
||||
fetchMock.mockReset()
|
||||
} )
|
||||
|
||||
it( 'retries transient 5xx errors and eventually resolves JSON', async () => {
|
||||
fetchMock
|
||||
.mockResolvedValueOnce( new Response( JSON.stringify({
|
||||
ok: false
|
||||
}), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
status: 502
|
||||
} ) )
|
||||
.mockResolvedValueOnce( new Response( JSON.stringify({
|
||||
ok: true
|
||||
}), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
status: 200
|
||||
} ) )
|
||||
|
||||
await expect( getJson( 'https://api.doesitarm.com/sitemap-endpoints.json', {
|
||||
attempts: 2,
|
||||
delayMs: 0
|
||||
} ) ).resolves.toEqual({
|
||||
ok: true
|
||||
} )
|
||||
|
||||
expect( fetchMock ).toHaveBeenCalledTimes( 2 )
|
||||
} )
|
||||
|
||||
it( 'does not retry non-5xx errors', async () => {
|
||||
fetchMock.mockResolvedValueOnce( new Response( JSON.stringify({
|
||||
ok: false
|
||||
}), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
status: 404
|
||||
} ) )
|
||||
|
||||
await expect( getJson( 'https://api.doesitarm.com/sitemap-endpoints.json', {
|
||||
attempts: 3,
|
||||
delayMs: 0
|
||||
} ) ).rejects.toMatchObject({
|
||||
response: {
|
||||
status: 404
|
||||
}
|
||||
})
|
||||
|
||||
expect( fetchMock ).toHaveBeenCalledTimes( 1 )
|
||||
} )
|
||||
|
||||
it( 'returns text responses', async () => {
|
||||
fetchMock.mockResolvedValueOnce( new Response( '<xml />', {
|
||||
headers: {
|
||||
'Content-Type': 'application/xml'
|
||||
},
|
||||
status: 200
|
||||
} ) )
|
||||
|
||||
await expect( getText( 'https://doesitarm.com/sitemap.xml' ) ).resolves.toBe( '<xml />' )
|
||||
} )
|
||||
|
||||
it( 'posts JSON payloads and parses JSON responses', async () => {
|
||||
fetchMock.mockResolvedValueOnce( new Response( JSON.stringify({
|
||||
supportedVersionNumber: '2.1.0'
|
||||
}), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
status: 200
|
||||
} ) )
|
||||
|
||||
await expect( postJson( 'https://doesitarm.com/api/test-results', {
|
||||
filename: 'App.zip'
|
||||
} ) ).resolves.toEqual({
|
||||
supportedVersionNumber: '2.1.0'
|
||||
} )
|
||||
|
||||
expect( fetchMock ).toHaveBeenCalledWith(
|
||||
'https://doesitarm.com/api/test-results',
|
||||
expect.objectContaining({
|
||||
body: JSON.stringify({
|
||||
filename: 'App.zip'
|
||||
}),
|
||||
method: 'POST'
|
||||
})
|
||||
)
|
||||
} )
|
||||
|
||||
it( 'supports config-object JSON requests for the API client', async () => {
|
||||
fetchMock.mockResolvedValueOnce( new Response( JSON.stringify({
|
||||
ok: true
|
||||
}), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
status: 200
|
||||
} ) )
|
||||
|
||||
await expect( requestJson({
|
||||
method: 'GET',
|
||||
url: 'https://doesitarm.com/api/app/spotify.json'
|
||||
}) ).resolves.toEqual({
|
||||
ok: true
|
||||
} )
|
||||
} )
|
||||
|
||||
it( 'maps head requests to booleans', async () => {
|
||||
fetchMock
|
||||
.mockResolvedValueOnce( new Response( null, {
|
||||
status: 200
|
||||
} ) )
|
||||
.mockResolvedValueOnce( new Response( null, {
|
||||
status: 404
|
||||
} ) )
|
||||
|
||||
await expect( headOk( 'https://doesitarm.com/sitemap.xml' ) ).resolves.toBe( true )
|
||||
await expect( headOk( 'https://doesitarm.com/missing.xml' ) ).resolves.toBe( false )
|
||||
} )
|
||||
|
||||
it( 'classifies retryable errors by HTTP status', () => {
|
||||
expect( shouldRetryError( {
|
||||
response: {
|
||||
status: 502
|
||||
}
|
||||
} ) ).toBe( true )
|
||||
expect( shouldRetryError( {
|
||||
response: {
|
||||
status: 503
|
||||
}
|
||||
} ) ).toBe( true )
|
||||
expect( shouldRetryError( {
|
||||
response: {
|
||||
status: 404
|
||||
}
|
||||
} ) ).toBe( false )
|
||||
expect( shouldRetryError( new Error( 'network' ) ) ).toBe( false )
|
||||
} )
|
||||
} )
|
||||
|
|
@ -6,89 +6,63 @@ import {
|
|||
vi
|
||||
} from 'vitest'
|
||||
|
||||
import axios from 'axios'
|
||||
import fs from 'fs-extra'
|
||||
|
||||
import {
|
||||
fetchJsonWithRetries,
|
||||
shouldRetryError
|
||||
loadSitemapEndpoints
|
||||
} from '~/helpers/pagefind/load-sitemap-endpoints'
|
||||
import { getJson } from '~/helpers/http.js'
|
||||
|
||||
vi.mock( 'axios', () => {
|
||||
vi.mock( 'fs-extra', () => {
|
||||
return {
|
||||
default: {
|
||||
get: vi.fn()
|
||||
pathExists: vi.fn(),
|
||||
readJson: vi.fn()
|
||||
}
|
||||
}
|
||||
} )
|
||||
|
||||
describe( 'load sitemap endpoints helper', () => {
|
||||
vi.mock( '~/helpers/http.js', () => {
|
||||
return {
|
||||
getJson: vi.fn(),
|
||||
shouldRetryError: vi.fn()
|
||||
}
|
||||
} )
|
||||
|
||||
describe( 'load sitemap endpoints', () => {
|
||||
beforeEach( () => {
|
||||
vi.mocked( axios.get ).mockReset()
|
||||
vi.mocked( fs.pathExists ).mockReset()
|
||||
vi.mocked( fs.readJson ).mockReset()
|
||||
vi.mocked( getJson ).mockReset()
|
||||
} )
|
||||
|
||||
it( 'retries transient 5xx errors and eventually resolves', async () => {
|
||||
const axiosGet = vi.mocked( axios.get )
|
||||
|
||||
axiosGet
|
||||
.mockRejectedValueOnce({
|
||||
response: {
|
||||
status: 502
|
||||
}
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
ok: true
|
||||
}
|
||||
} )
|
||||
|
||||
const data = await fetchJsonWithRetries( 'https://api.doesitarm.com/sitemap-endpoints.json', {
|
||||
attempts: 2,
|
||||
delayMs: 0
|
||||
it( 'reads the local sitemap-endpoints file when it exists', async () => {
|
||||
vi.mocked( fs.pathExists ).mockResolvedValueOnce( true )
|
||||
vi.mocked( fs.readJson ).mockResolvedValueOnce({
|
||||
endpoints: [ '/api/app/spotify.json' ]
|
||||
} )
|
||||
|
||||
expect( data ).toEqual({
|
||||
ok: true
|
||||
} )
|
||||
expect( axiosGet ).toHaveBeenCalledTimes( 2 )
|
||||
} )
|
||||
|
||||
it( 'does not retry non-5xx errors', async () => {
|
||||
const axiosGet = vi.mocked( axios.get )
|
||||
|
||||
axiosGet.mockRejectedValueOnce({
|
||||
response: {
|
||||
status: 404
|
||||
}
|
||||
await expect( loadSitemapEndpoints() ).resolves.toEqual({
|
||||
endpoints: [ '/api/app/spotify.json' ]
|
||||
})
|
||||
|
||||
await expect( fetchJsonWithRetries( 'https://api.doesitarm.com/sitemap-endpoints.json', {
|
||||
expect( getJson ).not.toHaveBeenCalled()
|
||||
} )
|
||||
|
||||
it( 'falls back to the remote sitemap-endpoints JSON when the local file is missing', async () => {
|
||||
vi.mocked( fs.pathExists ).mockResolvedValueOnce( false )
|
||||
vi.mocked( getJson ).mockResolvedValueOnce({
|
||||
endpoints: [ '/api/app/electron-framework.json' ]
|
||||
} )
|
||||
process.env.PUBLIC_API_DOMAIN = 'https://api.doesitarm.com'
|
||||
|
||||
await expect( loadSitemapEndpoints() ).resolves.toEqual({
|
||||
endpoints: [ '/api/app/electron-framework.json' ]
|
||||
} )
|
||||
|
||||
expect( getJson ).toHaveBeenCalledWith( 'https://api.doesitarm.com/sitemap-endpoints.json', {
|
||||
attempts: 3,
|
||||
delayMs: 0
|
||||
} ) ).rejects.toEqual({
|
||||
response: {
|
||||
status: 404
|
||||
}
|
||||
delayMs: 1000
|
||||
})
|
||||
|
||||
expect( axiosGet ).toHaveBeenCalledTimes( 1 )
|
||||
} )
|
||||
|
||||
it( 'classifies retryable server errors', () => {
|
||||
expect( shouldRetryError( {
|
||||
response: {
|
||||
status: 502
|
||||
}
|
||||
} ) ).toBe( true )
|
||||
expect( shouldRetryError( {
|
||||
response: {
|
||||
status: 503
|
||||
}
|
||||
} ) ).toBe( true )
|
||||
expect( shouldRetryError( {
|
||||
response: {
|
||||
status: 404
|
||||
}
|
||||
} ) ).toBe( false )
|
||||
expect( shouldRetryError( new Error( 'network' ) ) ).toBe( false )
|
||||
} )
|
||||
} )
|
||||
|
|
|
|||
|
|
@ -1,20 +1,16 @@
|
|||
import { Buffer } from 'buffer'
|
||||
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
parseFileSync,
|
||||
parsePlistBuffer
|
||||
parseFileSync,
|
||||
parsePlistBuffer,
|
||||
} from '~/helpers/scanner/parsers/plist-parser'
|
||||
|
||||
type ParsedPlist = Record<string, string>
|
||||
|
||||
const xmlPlist = Buffer.from( [
|
||||
const xmlPlist = Buffer.from(
|
||||
[
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
||||
'<plist version="1.0">',
|
||||
|
|
@ -24,29 +20,34 @@ const xmlPlist = Buffer.from( [
|
|||
' <key>CFBundleIdentifier</key>',
|
||||
' <string>com.doesitarm.playwright-native-app</string>',
|
||||
'</dict>',
|
||||
'</plist>'
|
||||
].join( '\n' ), 'utf8' )
|
||||
'</plist>',
|
||||
].join('\n'),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
describe( 'plist parser', () => {
|
||||
it( 'parses xml plist buffers asynchronously', async () => {
|
||||
const callback = vi.fn()
|
||||
const plist = await parsePlistBuffer( xmlPlist as any, callback ) as ParsedPlist
|
||||
describe('plist parser', () => {
|
||||
it('parses xml plist buffers asynchronously', async () => {
|
||||
const callback = vi.fn()
|
||||
const plist = (await parsePlistBuffer(
|
||||
xmlPlist as any,
|
||||
callback,
|
||||
)) as ParsedPlist
|
||||
|
||||
expect( plist.CFBundleExecutable ).toBe( 'Playwright Native App' )
|
||||
expect( plist.CFBundleIdentifier ).toBe( 'com.doesitarm.playwright-native-app' )
|
||||
expect( callback ).toHaveBeenCalledWith( null, plist )
|
||||
} )
|
||||
expect(plist.CFBundleExecutable).toBe('Playwright Native App')
|
||||
expect(plist.CFBundleIdentifier).toBe('com.doesitarm.playwright-native-app')
|
||||
expect(callback).toHaveBeenCalledWith(null, plist)
|
||||
})
|
||||
|
||||
it( 'parses xml plist buffers synchronously', () => {
|
||||
const plist = parseFileSync( xmlPlist as any ) as ParsedPlist
|
||||
it('parses xml plist buffers synchronously', () => {
|
||||
const plist = parseFileSync(xmlPlist as any) as ParsedPlist
|
||||
|
||||
expect( plist.CFBundleExecutable ).toBe( 'Playwright Native App' )
|
||||
expect( plist.CFBundleIdentifier ).toBe( 'com.doesitarm.playwright-native-app' )
|
||||
} )
|
||||
expect(plist.CFBundleExecutable).toBe('Playwright Native App')
|
||||
expect(plist.CFBundleIdentifier).toBe('com.doesitarm.playwright-native-app')
|
||||
})
|
||||
|
||||
it( 'rejects invalid plist data', async () => {
|
||||
await expect( parsePlistBuffer( Buffer.from( 'not-a-plist', 'utf8' ) as any ) )
|
||||
.rejects
|
||||
.toThrow( /Invalid binary plist/i )
|
||||
} )
|
||||
} )
|
||||
it('rejects invalid plist data', async () => {
|
||||
await expect(
|
||||
parsePlistBuffer(Buffer.from('not-a-plist', 'utf8') as any),
|
||||
).rejects.toThrow(/Invalid binary plist/i)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue