Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions scripts/smoke-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ for route in $(curl -s localhost:3000/v1/routes | jq -r '.[].path'); do
# we can run these, because they're ICS, not GCal
;;

"/v1/news/named/stolaf" | "/v1/news/named/krlx")
# Validate news endpoints with Zod schema
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment doesn't add much value.

echo "validating $route with Zod schema"
RESPONSE=$(curl --silent --fail "localhost:3000$route")
# Validate against the Zod schema
echo "$RESPONSE" | node dist/scripts/validate-schema.js "$route"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only place in this shell script where we depend on the script having somehow gotten to this path. We should not begin the tests unless the script is present here.

Are the Zod checks applied on ccc output (responses we send to apps) or input (responses we get from upstreams?)?

If they're applied on upstreams, then this test is also not actually testing what we want — in that case it's testing that… the output happens to also conform to the input spec? Which doesn't apply to other types of routes like food menus.

Why not just let these two routes

continue
;;

"/v1/calendar/"* | "/v1/convos/upcoming")
echo "skip because we don't have authorization during smoke tests"
continue
Expand Down
80 changes: 80 additions & 0 deletions scripts/validate-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env node
/**
* Script to validate JSON responses against Zod schemas
* Usage: node dist/scripts/validate-schema.js <schema-path> <json-data>
*
* Example:
* echo '{"title": "test"}' | node dist/scripts/validate-schema.js feeds/types FeedItemSchema
*/

import {readFileSync} from 'fs'
import {z} from 'zod'

// Schema registry - maps endpoint patterns to their schemas
const SCHEMA_REGISTRY: Record<string, {module: string; schema: string; isArray?: boolean}> = {
'/v1/news/named/stolaf': {module: 'feeds/types', schema: 'FeedItemSchema', isArray: true},
'/v1/news/named/mess': {module: 'feeds/types', schema: 'FeedItemSchema', isArray: true},
'/v1/news/named/krlx': {module: 'feeds/types', schema: 'FeedItemSchema', isArray: true},
'/v1/news/named/oleville': {module: 'feeds/types', schema: 'FeedItemSchema', isArray: true},
}

async function main() {
const endpoint = process.argv[2]
const jsonInput = process.argv[3] ?? readFileSync(0, 'utf-8') // Read from stdin if not provided

if (!endpoint) {
console.error('Usage: validate-schema <endpoint> [json-data]')
console.error('If json-data is not provided, reads from stdin')
process.exit(1)
}

const schemaInfo = SCHEMA_REGISTRY[endpoint]
if (!schemaInfo) {
console.error(`No schema registered for endpoint: ${endpoint}`)
console.error(`Registered endpoints: ${Object.keys(SCHEMA_REGISTRY).join(', ')}`)
process.exit(1)
}

try {
// Dynamically import the schema module
const modulePath = `../source/${schemaInfo.module}.js`
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const module = await import(modulePath)

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
let schema = module[schemaInfo.schema]
if (!schema) {
console.error(`Schema ${schemaInfo.schema} not found in module ${schemaInfo.module}`)
process.exit(1)
}

// Wrap in array if needed
if (schemaInfo.isArray) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
schema = z.array(schema)
}

// Parse the JSON input
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const data = JSON.parse(jsonInput)

// Validate against schema
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
schema.parse(data)

console.log('✓ Validation successful')
process.exit(0)
} catch (error) {
if (error instanceof z.ZodError) {
console.error('✗ Schema validation failed:')
console.error(JSON.stringify(error.errors, null, 2))
} else if (error instanceof SyntaxError) {
console.error('✗ Invalid JSON input')
} else {
console.error('✗ Validation error:', error)
}
process.exit(1)
}
}

void main()
16 changes: 10 additions & 6 deletions source/ccci-carleton-college/v1/menu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,17 @@ const cafeMenuFunctions: Record<keyof typeof menu.CAFE_URLS, (c: Context) => Pro
} as const

for (const cafe of keysOf(menu.CAFE_URLS)) {
test(`${cafe} cafe endpoint should return a BamcoCafeInfo struct`, async () => {
const ctx = {cacheControl: noop, body: null} as Context
await cafeInfoFunctions[cafe](ctx)
expect(() => CafeInfoResponseSchema.parse(ctx.body)).not.toThrow()
})
test(
`${cafe} cafe endpoint should return a BamcoCafeInfo struct`,
{timeout: 15_000},
async () => {
const ctx = {cacheControl: noop, body: null} as Context
await cafeInfoFunctions[cafe](ctx)
expect(() => CafeInfoResponseSchema.parse(ctx.body)).not.toThrow()
},
)

test(`${cafe} menu endpoint should return a CafeMenu struct`, async () => {
test(`${cafe} menu endpoint should return a CafeMenu struct`, {timeout: 15_000}, async () => {
const ctx = {cacheControl: noop, body: null} as Context
await cafeMenuFunctions[cafe](ctx)
expect(() => CafeMenuResponseSchema.parse(ctx.body)).not.toThrow()
Expand Down
16 changes: 10 additions & 6 deletions source/ccci-stolaf-college/v1/menu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,17 @@ const cafeMenuFunctions: Record<keyof typeof menu.CAFE_URLS, (c: Context) => Pro
} as const

for (const cafe of keysOf(menu.CAFE_URLS)) {
test(`${cafe} cafe endpoint should return a BamcoCafeInfo struct`, async () => {
const ctx = {cacheControl: noop, body: null} as Context
await cafeInfoFunctions[cafe](ctx)
expect(() => CafeInfoResponseSchema.parse(ctx.body)).not.toThrow()
})
test(
`${cafe} cafe endpoint should return a BamcoCafeInfo struct`,
{timeout: 15_000},
async () => {
const ctx = {cacheControl: noop, body: null} as Context
await cafeInfoFunctions[cafe](ctx)
expect(() => CafeInfoResponseSchema.parse(ctx.body)).not.toThrow()
},
)

test(`${cafe} menu endpoint should return a CafeMenu struct`, async () => {
test(`${cafe} menu endpoint should return a CafeMenu struct`, {timeout: 15_000}, async () => {
const ctx = {cacheControl: noop, body: null} as Context
await cafeMenuFunctions[cafe](ctx)
expect(() => CafeMenuResponseSchema.parse(ctx.body)).not.toThrow()
Expand Down
Loading