Skip to content

fix(deps): update dependency zod to v4 #488

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 5, 2025
Merged
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
2 changes: 1 addition & 1 deletion packages/airnode-feed/config/airnode-feed.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
],
"ois": [
{
"oisFormat": "2.3.0",
"oisFormat": "3.0.0",
"title": "Nodary",
"version": "0.2.0",
"apiSpecifications": {
Expand Down
4 changes: 2 additions & 2 deletions packages/airnode-feed/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@
"@api3/airnode-node": "^0.15.0",
"@api3/airnode-validator": "^0.15.0",
"@api3/commons": "^0.13.4",
"@api3/ois": "^2.3.2",
"@api3/ois": "^3.0.0",
"@api3/promise-utils": "^0.4.0",
"axios": "^1.11.0",
"dotenv": "^17.2.1",
"ethers": "^5.8.0",
"express": "^5.1.0",
"lodash": "^4.17.21",
"zod": "^3.25.76"
"zod": "^4.0.5"
},
"devDependencies": {
"@types/express": "^5.0.3",
Expand Down
10 changes: 5 additions & 5 deletions packages/airnode-feed/src/api-requests/data-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe(makeTemplateRequests.name, () => {

jest.useFakeTimers().setSystemTime(new Date('2023-01-20')); // 1674172800

const response = await makeTemplateRequests(config.triggers.signedApiUpdates[0]);
const response = await makeTemplateRequests(config.triggers.signedApiUpdates[0]!);

expect(response).toStrictEqual(nodaryTemplateResponses);
expect(adapterModule.buildAndExecuteRequest).toHaveBeenCalledTimes(1);
Expand All @@ -36,7 +36,7 @@ describe(makeTemplateRequests.name, () => {
jest.spyOn(logger, 'warn');
jest.spyOn(adapterModule, 'buildAndExecuteRequest').mockRejectedValue(nodaryTemplateRequestError);

await makeTemplateRequests(config.triggers.signedApiUpdates[0]);
await makeTemplateRequests(config.triggers.signedApiUpdates[0]!);

expect(logger.warn).toHaveBeenCalledTimes(1);
expect(logger.warn).toHaveBeenCalledWith('Failed to make API call.', {
Expand Down Expand Up @@ -89,7 +89,7 @@ describe(makeTemplateRequests.name, () => {
jest.spyOn(stateModule, 'getState').mockReturnValue(state);
jest.mocked(axios).mockRejectedValue(new Error('network error'));

await makeTemplateRequests(config.triggers.signedApiUpdates[0]);
await makeTemplateRequests(config.triggers.signedApiUpdates[0]!);

expect(axios).toHaveBeenCalledTimes(1);
expect(axios).toHaveBeenCalledWith({
Expand Down Expand Up @@ -141,7 +141,7 @@ describe(makeTemplateRequests.name, () => {

const buildAndExecuteRequestSpy = jest.spyOn(adapterModule, 'buildAndExecuteRequest');

const makeTemplateRequestsResult = await makeTemplateRequests(config.triggers.signedApiUpdates[0]);
const makeTemplateRequestsResult = await makeTemplateRequests(config.triggers.signedApiUpdates[0]!);

expect(axios).toHaveBeenCalledTimes(0);
expect(buildAndExecuteRequestSpy).not.toHaveBeenCalled();
Expand Down Expand Up @@ -181,7 +181,7 @@ describe(makeTemplateRequests.name, () => {

const buildAndExecuteRequestSpy = jest.spyOn(adapterModule, 'buildAndExecuteRequest');

const makeTemplateRequestsResult = await makeTemplateRequests(config.triggers.signedApiUpdates[0]);
const makeTemplateRequestsResult = await makeTemplateRequests(config.triggers.signedApiUpdates[0]!);

expect(axios).toHaveBeenCalledTimes(0);
expect(buildAndExecuteRequestSpy).not.toHaveBeenCalled();
Expand Down
23 changes: 1 addition & 22 deletions packages/airnode-feed/src/api-requests/signed-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,28 +35,7 @@ describe(pushSignedData.name, () => {

expect(response).toStrictEqual([{ success: false }]);
expect(logger.warn).toHaveBeenCalledWith('Failed to parse response from the signed API.', {
errors: new ZodError([
{
code: 'invalid_type',
expected: 'number',
received: 'undefined',
path: ['count'],
message: 'Required',
},
{
code: 'invalid_type',
expected: 'number',
received: 'undefined',
path: ['skipped'],
message: 'Required',
},
{
code: 'unrecognized_keys',
keys: ['strange'],
path: [],
message: "Unrecognized key(s) in object: 'strange'",
},
]),
errors: expect.any(ZodError),
Copy link
Contributor

Choose a reason for hiding this comment

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

I added a new test in schema.test.ts for this.

});
});

Expand Down
4 changes: 2 additions & 2 deletions packages/airnode-feed/src/heartbeat/heartbeat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ describe(logHeartbeat.name, () => {
// NOTE: This tests will fail each time the example config changes (except for the nodeVersion). This should be
// quite rare and the test verifies that the heartbeat sends correct data.
const expectedHeartbeat = {
configHash: '0xce9f58b6927572c9cc6e3bb532e6bd54899c8b3a2c15da18ee556599f1b3249f',
configHash: '0x5c1e8b1f41bf76f010c36a89b73ed4ee5afbb214d7118ac3cdb11e1ad5901982',
airnode: '0xbF3137b0a7574563a23a8fC8badC6537F98197CC',
signature:
'0x29e4b58fe8ebac9a7c26de469978707b5d291eac06b76b4e8cca501011360de879d2e3dc498fd84e19d4c4a57b809e0e32f6855c2a834a38167dac5f891f159c1c',
'0x21f9249611d0f320f96ee057c7ba36aad4c5acaf8c10e8d50219aba3e2b714d55c1fd6b4207c5ed78cdeb925b31684cf4d00a2445e9c6665d952686b3db9c0e81c',
stage: 'test',
nodeVersion: '0.7.0',
currentTimestamp: '1674172803',
Expand Down
3 changes: 2 additions & 1 deletion packages/airnode-feed/src/validation/env.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { join } from 'node:path';

import dotenv from 'dotenv';
import { z } from 'zod';

import { type EnvConfig, envConfigSchema } from './schema';

Expand All @@ -13,7 +14,7 @@ export const loadEnv = () => {

const parseResult = envConfigSchema.safeParse(process.env);
if (!parseResult.success) {
throw new Error(`Invalid environment variables:\n, ${JSON.stringify(parseResult.error.format())}`);
throw new Error(`Invalid environment variables: ${z.prettifyError(parseResult.error)}`);
}

env = parseResult.data;
Expand Down
137 changes: 78 additions & 59 deletions packages/airnode-feed/src/validation/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,23 @@ import { join } from 'node:path';

import { interpolateSecretsIntoConfig } from '@api3/commons';
import dotenv from 'dotenv';
import { ZodError } from 'zod';

import { config } from '../../test/fixtures';

import { type Config, configSchema, signedApisSchema } from './schema';
import { type Config, configSchema, signedApiResponseSchema, signedApisSchema } from './schema';

test('validates example config', async () => {
const exampleConfig = JSON.parse(readFileSync(join(__dirname, '../../config/airnode-feed.example.json'), 'utf8'));

// The mnemonic is not interpolated (and thus invalid).
await expect(configSchema.parseAsync(exampleConfig)).rejects.toStrictEqual(
new ZodError([
{
code: 'custom',
message: 'Invalid mnemonic',
path: ['nodeSettings', 'airnodeWalletMnemonic'],
},
])
);
const result = await configSchema.safeParseAsync(exampleConfig);
expect(result.error?.issues).toStrictEqual([
{
code: 'custom',
message: 'Invalid mnemonic',
path: ['nodeSettings', 'airnodeWalletMnemonic'],
},
]);

const exampleSecrets = dotenv.parse(readFileSync(join(__dirname, '../../config/secrets.example.env'), 'utf8'));
await expect(
Expand All @@ -38,32 +36,29 @@ test('ensures nodeVersion matches Airnode feed version', async () => {
},
};

await expect(configSchema.parseAsync(invalidConfig)).rejects.toStrictEqual(
new ZodError([
{
code: 'custom',
message: 'Invalid node version',
path: ['nodeSettings', 'nodeVersion'],
},
])
);
const result = await configSchema.safeParseAsync(invalidConfig);
expect(result.error?.issues).toStrictEqual([
{
code: 'custom',
message: 'Invalid node version',
path: ['nodeSettings', 'nodeVersion'],
},
]);
});

test('ensures signed API names are unique', () => {
expect(() =>
signedApisSchema.parse([
expect(
signedApisSchema.safeParse([
{ name: 'foo', url: 'https://example.com', authToken: null },
{ name: 'foo', url: 'https://example.com', authToken: null },
])
).toThrow(
new ZodError([
{
code: 'custom',
message: 'Signed API names must be unique',
path: ['signedApis'],
},
])
);
]).error?.issues
).toStrictEqual([
{
code: 'custom',
message: 'Signed API names must be unique',
path: ['signedApis'],
},
]);

expect(signedApisSchema.parse([{ name: 'foo', url: 'https://example.com', authToken: null }])).toStrictEqual([
{
Expand All @@ -90,15 +85,14 @@ describe('validateTriggerReferences', () => {
},
};

await expect(configSchema.parseAsync(invalidConfig)).rejects.toStrictEqual(
new ZodError([
{
code: 'custom',
message: `Template "${notFoundTemplateId}" is not defined in the config.templates object`,
path: ['triggers', 'signedApiUpdates', 0, 'templateIds', 0],
},
])
);
const result = await configSchema.safeParseAsync(invalidConfig);
expect(result.error?.issues).toStrictEqual([
{
code: 'custom',
message: `Template "${notFoundTemplateId}" is not defined in the config.templates object`,
path: ['triggers', 'signedApiUpdates', 0, 'templateIds', 0],
},
]);
});

it('validates all templates reference the same endpoint', async () => {
Expand Down Expand Up @@ -150,15 +144,14 @@ describe('validateTriggerReferences', () => {
],
};

await expect(configSchema.parseAsync(invalidConfig)).rejects.toStrictEqual(
new ZodError([
{
code: 'custom',
message: 'The endpoint utilized by each template must be same',
path: ['triggers', 'signedApiUpdates', 0, 'templateIds'],
},
])
);
const result = await configSchema.safeParseAsync(invalidConfig);
expect(result.error?.issues).toStrictEqual([
{
code: 'custom',
message: 'The endpoint utilized by each template must be same',
path: ['triggers', 'signedApiUpdates', 0, 'templateIds'],
},
]);
});

it('validates operation effects are identical', async () => {
Expand All @@ -170,15 +163,14 @@ describe('validateTriggerReferences', () => {
],
};

await expect(configSchema.parseAsync(invalidConfig)).rejects.toStrictEqual(
new ZodError([
{
code: 'custom',
message: 'The endpoint utilized by each template must have the same operation effect',
path: ['triggers', 'signedApiUpdates', 0, 'templateIds'],
},
])
);
const result = await configSchema.safeParseAsync(invalidConfig);
expect(result.error?.issues).toStrictEqual([
{
code: 'custom',
message: 'The endpoint utilized by each template must have the same operation effect',
path: ['triggers', 'signedApiUpdates', 0, 'templateIds'],
},
]);
});

it('skips operation effect validation for skip API call endpoints', async () => {
Expand All @@ -204,3 +196,30 @@ describe('validateTriggerReferences', () => {
await expect(configSchema.parseAsync(skipApiCallConfig)).resolves.toBeDefined();
});
});

describe('signedApiResponseSchema', () => {
it('validates that the expected keys are present', () => {
expect(signedApiResponseSchema.safeParse({ strange: 'some-invalid-response' }).error?.issues).toStrictEqual([
{
code: 'invalid_type',
expected: 'number',
path: ['count'],
message: 'Invalid input: expected number, received undefined',
},
{
code: 'invalid_type',
expected: 'number',
path: ['skipped'],
message: 'Invalid input: expected number, received undefined',
},
{
code: 'unrecognized_keys',
keys: ['strange'],
path: [],
message: 'Unrecognized key: "strange"',
},
]);

expect(() => signedApiResponseSchema.parse({ count: 12, skipped: 3 })).not.toThrow();
});
});
Loading