easctl 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/README.md +195 -0
  3. package/dist/index.js +31401 -0
  4. package/dist/index.js.map +1 -0
  5. package/manual-test/package-lock.json +4483 -0
  6. package/manual-test/package.json +15 -0
  7. package/package.json +40 -0
  8. package/src/__tests__/chains.test.ts +82 -0
  9. package/src/__tests__/clear-key.test.ts +40 -0
  10. package/src/__tests__/client.test.ts +168 -0
  11. package/src/__tests__/commands/attest.test.ts +203 -0
  12. package/src/__tests__/commands/get-attestation.test.ts +164 -0
  13. package/src/__tests__/commands/multi-attest.test.ts +166 -0
  14. package/src/__tests__/commands/multi-revoke.test.ts +114 -0
  15. package/src/__tests__/commands/multi-timestamp.test.ts +88 -0
  16. package/src/__tests__/commands/offchain-attest.test.ts +217 -0
  17. package/src/__tests__/commands/query-attestation.test.ts +84 -0
  18. package/src/__tests__/commands/query-attestations.test.ts +156 -0
  19. package/src/__tests__/commands/query-schema.test.ts +62 -0
  20. package/src/__tests__/commands/query-schemas.test.ts +110 -0
  21. package/src/__tests__/commands/revoke.test.ts +86 -0
  22. package/src/__tests__/commands/schema-get.test.ts +66 -0
  23. package/src/__tests__/commands/schema-register.test.ts +94 -0
  24. package/src/__tests__/commands/timestamp.test.ts +78 -0
  25. package/src/__tests__/config.test.ts +103 -0
  26. package/src/__tests__/graphql.test.ts +148 -0
  27. package/src/__tests__/integration/graphql-live.test.ts +103 -0
  28. package/src/__tests__/integration/offchain-signing.test.ts +252 -0
  29. package/src/__tests__/integration/schema-encoder.test.ts +131 -0
  30. package/src/__tests__/output.test.ts +138 -0
  31. package/src/__tests__/set-key.test.ts +58 -0
  32. package/src/__tests__/stdin.test.ts +15 -0
  33. package/src/chains.ts +99 -0
  34. package/src/client.ts +53 -0
  35. package/src/commands/attest.ts +73 -0
  36. package/src/commands/clear-key.ts +15 -0
  37. package/src/commands/get-attestation.ts +58 -0
  38. package/src/commands/multi-attest.ts +75 -0
  39. package/src/commands/multi-revoke.ts +60 -0
  40. package/src/commands/multi-timestamp.ts +43 -0
  41. package/src/commands/offchain-attest.ts +78 -0
  42. package/src/commands/query-attestation.ts +31 -0
  43. package/src/commands/query-attestations.ts +57 -0
  44. package/src/commands/query-schema.ts +24 -0
  45. package/src/commands/query-schemas.ts +35 -0
  46. package/src/commands/revoke.ts +48 -0
  47. package/src/commands/schema-get.ts +30 -0
  48. package/src/commands/schema-register.ts +49 -0
  49. package/src/commands/set-key.ts +19 -0
  50. package/src/commands/timestamp.ts +35 -0
  51. package/src/config.ts +41 -0
  52. package/src/graphql.ts +136 -0
  53. package/src/index.ts +74 -0
  54. package/src/output.ts +50 -0
  55. package/src/stdin.ts +15 -0
  56. package/src/validation.ts +15 -0
  57. package/tsconfig.json +16 -0
  58. package/tsup.config.ts +21 -0
  59. package/vitest.config.ts +7 -0
@@ -0,0 +1,217 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ const mockSignOffchainAttestation = vi.fn().mockResolvedValue({
4
+ uid: '0xoffchainuid',
5
+ sig: '0xsig',
6
+ });
7
+ const mockGetOffchain = vi.fn().mockResolvedValue({
8
+ signOffchainAttestation: mockSignOffchainAttestation,
9
+ });
10
+ const mockSigner = { address: '0xSignerAddr' };
11
+ const mockClient = {
12
+ eas: { getOffchain: mockGetOffchain },
13
+ signer: mockSigner,
14
+ address: '0xAttester',
15
+ };
16
+
17
+ vi.mock('../../client.js', () => ({
18
+ createEASClient: vi.fn(() => mockClient),
19
+ }));
20
+
21
+ vi.mock('../../output.js', () => ({
22
+ output: vi.fn(),
23
+ handleError: vi.fn(),
24
+ }));
25
+
26
+ vi.mock('../../stdin.js', () => ({
27
+ resolveInput: vi.fn((v: string) => Promise.resolve(v)),
28
+ }));
29
+
30
+ vi.mock('../../validation.js', () => ({
31
+ validateAddress: vi.fn(),
32
+ validateBytes32: vi.fn(),
33
+ }));
34
+
35
+ vi.mock('../../graphql.js', () => ({
36
+ EASSCAN_URLS: {
37
+ ethereum: 'https://easscan.org',
38
+ base: 'https://base.easscan.org',
39
+ sepolia: 'https://sepolia.easscan.org',
40
+ },
41
+ }));
42
+
43
+ const mockEncodeData = vi.fn().mockReturnValue('0xencoded');
44
+
45
+ vi.mock('@ethereum-attestation-service/eas-sdk', () => ({
46
+ SchemaEncoder: class MockSchemaEncoder {
47
+ encodeData = (...args: any[]) => mockEncodeData(...args);
48
+ },
49
+ NO_EXPIRATION: 0n,
50
+ ZERO_BYTES32: '0x0000000000000000000000000000000000000000000000000000000000000000',
51
+ createOffchainURL: vi.fn().mockReturnValue('/offchain/url/123'),
52
+ }));
53
+
54
+ import { offchainAttestCommand } from '../../commands/offchain-attest.js';
55
+ import { createEASClient } from '../../client.js';
56
+ import { output, handleError } from '../../output.js';
57
+ import { createOffchainURL } from '@ethereum-attestation-service/eas-sdk';
58
+
59
+ describe('offchain-attest command', () => {
60
+ beforeEach(() => vi.clearAllMocks());
61
+
62
+ async function runCommand(args: string[]) {
63
+ await offchainAttestCommand.parseAsync(['node', 'test', ...args]);
64
+ }
65
+
66
+ it('creates offchain attestation with correct signing args', async () => {
67
+ await runCommand([
68
+ '-s', '0xschema',
69
+ '-d', '[{"name":"x","type":"uint8","value":"1"}]',
70
+ '-r', '0xRecipient',
71
+ ]);
72
+
73
+ expect(mockGetOffchain).toHaveBeenCalled();
74
+ expect(mockEncodeData).toHaveBeenCalledWith([
75
+ { name: 'x', type: 'uint8', value: '1' },
76
+ ]);
77
+
78
+ // Verify signOffchainAttestation was called with correct params
79
+ const signCall = mockSignOffchainAttestation.mock.calls[0];
80
+ const params = signCall[0];
81
+ expect(params.schema).toBe('0xschema');
82
+ expect(params.recipient).toBe('0xRecipient');
83
+ expect(params.expirationTime).toBe(0n); // NO_EXPIRATION
84
+ expect(params.revocable).toBe(true);
85
+ expect(params.refUID).toBe('0x0000000000000000000000000000000000000000000000000000000000000000');
86
+ expect(params.data).toBe('0xencoded');
87
+ expect(typeof params.time).toBe('bigint');
88
+ expect(params.time).toBeGreaterThan(0n);
89
+
90
+ // Verify signer was passed
91
+ expect(signCall[1]).toBe(mockSigner);
92
+ });
93
+
94
+ it('passes createOffchainURL the correct package', async () => {
95
+ const signedAttestation = { uid: '0xoffchainuid', sig: '0xsig' };
96
+ mockSignOffchainAttestation.mockResolvedValueOnce(signedAttestation);
97
+
98
+ await runCommand([
99
+ '-s', '0xschema',
100
+ '-d', '[{"name":"x","type":"uint8","value":"1"}]',
101
+ ]);
102
+
103
+ expect(createOffchainURL).toHaveBeenCalledWith({
104
+ sig: signedAttestation,
105
+ signer: '0xAttester',
106
+ });
107
+ });
108
+
109
+ it('outputs uid, attester, chain, and offchainUrl', async () => {
110
+ await runCommand([
111
+ '-s', '0xschema',
112
+ '-d', '[{"name":"x","type":"uint8","value":"1"}]',
113
+ ]);
114
+
115
+ expect(output).toHaveBeenCalledWith({
116
+ success: true,
117
+ data: expect.objectContaining({
118
+ uid: '0xoffchainuid',
119
+ attester: '0xAttester',
120
+ chain: 'ethereum',
121
+ offchainUrl: 'https://easscan.org/offchain/url/123',
122
+ }),
123
+ });
124
+ });
125
+
126
+ it('handles invalid JSON in --data', async () => {
127
+ await runCommand(['-s', '0xschema', '-d', '{bad json']);
128
+ expect(handleError).toHaveBeenCalledWith(expect.any(Error));
129
+ const err = (handleError as any).mock.calls[0][0] as Error;
130
+ expect(err.message).toContain('Invalid JSON in --data');
131
+ });
132
+
133
+ it('uses chain-specific EASSCAN host', async () => {
134
+ await runCommand([
135
+ '-s', '0xschema',
136
+ '-d', '[{"name":"x","type":"uint8","value":"1"}]',
137
+ '-c', 'base',
138
+ ]);
139
+
140
+ expect(output).toHaveBeenCalledWith({
141
+ success: true,
142
+ data: expect.objectContaining({
143
+ offchainUrl: 'https://base.easscan.org/offchain/url/123',
144
+ }),
145
+ });
146
+ });
147
+
148
+ it('passes --no-revocable as false to signing', async () => {
149
+ await runCommand([
150
+ '-s', '0xschema',
151
+ '-d', '[{"name":"x","type":"uint8","value":"1"}]',
152
+ '--no-revocable',
153
+ ]);
154
+
155
+ const params = mockSignOffchainAttestation.mock.calls[0][0];
156
+ expect(params.revocable).toBe(false);
157
+ });
158
+
159
+ it('passes non-zero expiration as BigInt', async () => {
160
+ await runCommand([
161
+ '-s', '0xschema',
162
+ '-d', '[{"name":"x","type":"uint8","value":"1"}]',
163
+ '--expiration', '1700000000',
164
+ ]);
165
+
166
+ const params = mockSignOffchainAttestation.mock.calls[0][0];
167
+ expect(params.expirationTime).toBe(1700000000n);
168
+ });
169
+
170
+ it('uses custom chain and rpc-url', async () => {
171
+ await runCommand([
172
+ '-s', '0xschema',
173
+ '-d', '[{"name":"x","type":"uint8","value":"1"}]',
174
+ '-c', 'sepolia',
175
+ '--rpc-url', 'https://custom.rpc',
176
+ ]);
177
+
178
+ expect(createEASClient).toHaveBeenCalledWith('sepolia', 'https://custom.rpc');
179
+ });
180
+
181
+ it('passes --ref-uid to signing params', async () => {
182
+ await runCommand([
183
+ '-s', '0xschema',
184
+ '-d', '[{"name":"x","type":"uint8","value":"1"}]',
185
+ '--ref-uid', '0xreferenceduid',
186
+ ]);
187
+
188
+ const params = mockSignOffchainAttestation.mock.calls[0][0];
189
+ expect(params.refUID).toBe('0xreferenceduid');
190
+ });
191
+
192
+ it('includes full attestation object in output', async () => {
193
+ const signedAttestation = { uid: '0xoffchainuid', sig: '0xsig', message: {} };
194
+ mockSignOffchainAttestation.mockResolvedValueOnce(signedAttestation);
195
+
196
+ await runCommand([
197
+ '-s', '0xschema',
198
+ '-d', '[{"name":"x","type":"uint8","value":"1"}]',
199
+ ]);
200
+
201
+ const outputCall = (output as any).mock.calls[0][0];
202
+ expect(outputCall.data.attestation).toBe(signedAttestation);
203
+ });
204
+
205
+ it('passes SDK errors to handleError', async () => {
206
+ mockGetOffchain.mockRejectedValueOnce(new Error('network unavailable'));
207
+
208
+ await runCommand([
209
+ '-s', '0xschema',
210
+ '-d', '[{"name":"x","type":"uint8","value":"1"}]',
211
+ ]);
212
+
213
+ expect(handleError).toHaveBeenCalledWith(expect.any(Error));
214
+ const err = (handleError as any).mock.calls[0][0] as Error;
215
+ expect(err.message).toBe('network unavailable');
216
+ });
217
+ });
@@ -0,0 +1,84 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ vi.mock('../../graphql.js', () => ({
4
+ graphqlQuery: vi.fn(),
5
+ QUERIES: {
6
+ getAttestation: 'query GetAttestation($id: String!) { attestation(where: { id: $id }) { id } }',
7
+ },
8
+ }));
9
+
10
+ vi.mock('../../output.js', () => ({
11
+ output: vi.fn(),
12
+ handleError: vi.fn(),
13
+ }));
14
+
15
+ vi.mock('../../validation.js', () => ({
16
+ validateBytes32: vi.fn(),
17
+ }));
18
+
19
+ import { queryAttestationCommand } from '../../commands/query-attestation.js';
20
+ import { graphqlQuery, QUERIES } from '../../graphql.js';
21
+ import { output, handleError } from '../../output.js';
22
+
23
+ describe('query-attestation command', () => {
24
+ beforeEach(() => vi.clearAllMocks());
25
+
26
+ async function runCommand(args: string[]) {
27
+ await queryAttestationCommand.parseAsync(['node', 'test', ...args]);
28
+ }
29
+
30
+ it('queries and returns attestation', async () => {
31
+ (graphqlQuery as any).mockResolvedValue({
32
+ attestation: { id: '0xatt', attester: '0xAttester', data: '0xdata' },
33
+ });
34
+
35
+ await runCommand(['-u', '0xatt']);
36
+
37
+ expect(graphqlQuery).toHaveBeenCalledWith('ethereum', QUERIES.getAttestation, { id: '0xatt' });
38
+ expect(output).toHaveBeenCalledWith({
39
+ success: true,
40
+ data: expect.objectContaining({ id: '0xatt', attester: '0xAttester' }),
41
+ });
42
+ });
43
+
44
+ it('parses decodedDataJson when present', async () => {
45
+ (graphqlQuery as any).mockResolvedValue({
46
+ attestation: {
47
+ id: '0xatt',
48
+ decodedDataJson: '[{"name":"score","type":"uint256","value":{"value":"100"}}]',
49
+ },
50
+ });
51
+
52
+ await runCommand(['-u', '0xatt']);
53
+
54
+ const outputCall = (output as any).mock.calls[0][0];
55
+ expect(outputCall.data.decodedData).toEqual([
56
+ { name: 'score', type: 'uint256', value: { value: '100' } },
57
+ ]);
58
+ });
59
+
60
+ it('keeps raw data when decodedDataJson is invalid JSON', async () => {
61
+ (graphqlQuery as any).mockResolvedValue({
62
+ attestation: {
63
+ id: '0xatt',
64
+ decodedDataJson: 'not-json{{{',
65
+ },
66
+ });
67
+
68
+ await runCommand(['-u', '0xatt']);
69
+
70
+ const outputCall = (output as any).mock.calls[0][0];
71
+ expect(outputCall.data.decodedData).toBeUndefined();
72
+ expect(outputCall.data.decodedDataJson).toBe('not-json{{{');
73
+ });
74
+
75
+ it('throws when attestation not found', async () => {
76
+ (graphqlQuery as any).mockResolvedValue({ attestation: null });
77
+
78
+ await runCommand(['-u', '0xmissing']);
79
+
80
+ expect(handleError).toHaveBeenCalledWith(expect.any(Error));
81
+ const err = (handleError as any).mock.calls[0][0] as Error;
82
+ expect(err.message).toContain('not found');
83
+ });
84
+ });
@@ -0,0 +1,156 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ vi.mock('../../graphql.js', () => ({
4
+ graphqlQuery: vi.fn(),
5
+ QUERIES: {
6
+ getAttestationsBySchema: 'query BySchema',
7
+ getAttestationsByAttester: 'query ByAttester',
8
+ },
9
+ }));
10
+
11
+ vi.mock('../../output.js', () => ({
12
+ output: vi.fn(),
13
+ handleError: vi.fn(),
14
+ }));
15
+
16
+ vi.mock('../../validation.js', () => ({
17
+ validateAddress: vi.fn(),
18
+ validateBytes32: vi.fn(),
19
+ }));
20
+
21
+ import { queryAttestationsCommand } from '../../commands/query-attestations.js';
22
+ import { graphqlQuery, QUERIES } from '../../graphql.js';
23
+ import { output, handleError } from '../../output.js';
24
+
25
+ describe('query-attestations command', () => {
26
+ beforeEach(() => vi.clearAllMocks());
27
+
28
+ async function runCommand(args: string[]) {
29
+ await queryAttestationsCommand.parseAsync(['node', 'test', ...args]);
30
+ }
31
+
32
+ it('queries by schema with default skip', async () => {
33
+ (graphqlQuery as any).mockResolvedValue({
34
+ attestations: [{ id: '0x1' }, { id: '0x2' }],
35
+ });
36
+
37
+ await runCommand(['-s', '0xschema']);
38
+
39
+ expect(graphqlQuery).toHaveBeenCalledWith('ethereum', QUERIES.getAttestationsBySchema, {
40
+ schemaId: '0xschema',
41
+ take: 10,
42
+ skip: 0,
43
+ });
44
+ expect(output).toHaveBeenCalledWith({
45
+ success: true,
46
+ data: { count: 2, attestations: expect.any(Array) },
47
+ });
48
+ });
49
+
50
+ it('queries by attester with default skip', async () => {
51
+ (graphqlQuery as any).mockResolvedValue({
52
+ attestations: [{ id: '0x1' }],
53
+ });
54
+
55
+ await runCommand(['-a', '0xAttester']);
56
+
57
+ expect(graphqlQuery).toHaveBeenCalledWith('ethereum', QUERIES.getAttestationsByAttester, {
58
+ attester: '0xAttester',
59
+ take: 10,
60
+ skip: 0,
61
+ });
62
+ });
63
+
64
+ it('passes skip for pagination', async () => {
65
+ (graphqlQuery as any).mockResolvedValue({ attestations: [] });
66
+
67
+ await runCommand(['-s', '0xschema', '--skip', '20']);
68
+
69
+ expect(graphqlQuery).toHaveBeenCalledWith(
70
+ 'ethereum',
71
+ expect.any(String),
72
+ expect.objectContaining({ skip: 20 })
73
+ );
74
+ });
75
+
76
+ it('schema takes precedence when both provided', async () => {
77
+ (graphqlQuery as any).mockResolvedValue({ attestations: [] });
78
+
79
+ await runCommand(['-s', '0xschema', '-a', '0xAttester']);
80
+
81
+ expect(graphqlQuery).toHaveBeenCalledTimes(1);
82
+ expect(graphqlQuery).toHaveBeenCalledWith(
83
+ 'ethereum',
84
+ QUERIES.getAttestationsBySchema,
85
+ expect.objectContaining({ schemaId: '0xschema' })
86
+ );
87
+ });
88
+
89
+ it('throws when neither schema nor attester provided', async () => {
90
+ await runCommand([]);
91
+
92
+ expect(handleError).toHaveBeenCalledWith(expect.any(Error));
93
+ const err = (handleError as any).mock.calls[0][0] as Error;
94
+ expect(err.message).toContain('Provide at least one filter');
95
+ });
96
+
97
+ it('passes limit as integer', async () => {
98
+ (graphqlQuery as any).mockResolvedValue({ attestations: [] });
99
+
100
+ await runCommand(['-s', '0xschema', '-n', '25']);
101
+
102
+ expect(graphqlQuery).toHaveBeenCalledWith(
103
+ 'ethereum',
104
+ expect.any(String),
105
+ expect.objectContaining({ take: 25 })
106
+ );
107
+ });
108
+
109
+ it('parses decodedDataJson for each attestation', async () => {
110
+ (graphqlQuery as any).mockResolvedValue({
111
+ attestations: [
112
+ { id: '0x1', decodedDataJson: '{"key":"value"}' },
113
+ { id: '0x2', decodedDataJson: null },
114
+ ],
115
+ });
116
+
117
+ await runCommand(['-s', '0xschema']);
118
+
119
+ const outputCall = (output as any).mock.calls[0][0];
120
+ expect(outputCall.data.attestations[0].decodedData).toEqual({ key: 'value' });
121
+ expect(outputCall.data.attestations[1].decodedData).toBeUndefined();
122
+ });
123
+
124
+ it('handles invalid decodedDataJson gracefully', async () => {
125
+ (graphqlQuery as any).mockResolvedValue({
126
+ attestations: [{ id: '0x1', decodedDataJson: 'bad-json' }],
127
+ });
128
+
129
+ await runCommand(['-s', '0xschema']);
130
+
131
+ const outputCall = (output as any).mock.calls[0][0];
132
+ expect(outputCall.data.attestations[0].decodedData).toBeUndefined();
133
+ });
134
+
135
+ it('returns empty results with count 0', async () => {
136
+ (graphqlQuery as any).mockResolvedValue({ attestations: [] });
137
+
138
+ await runCommand(['-s', '0xschema']);
139
+
140
+ expect(output).toHaveBeenCalledWith({
141
+ success: true,
142
+ data: { count: 0, attestations: [] },
143
+ });
144
+ });
145
+
146
+ it('handles missing attestations key in response', async () => {
147
+ (graphqlQuery as any).mockResolvedValue({});
148
+
149
+ await runCommand(['-s', '0xschema']);
150
+
151
+ expect(output).toHaveBeenCalledWith({
152
+ success: true,
153
+ data: { count: 0, attestations: [] },
154
+ });
155
+ });
156
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ vi.mock('../../graphql.js', () => ({
4
+ graphqlQuery: vi.fn(),
5
+ QUERIES: {
6
+ getSchema: 'query GetSchema($id: String!) { schema(where: { id: $id }) { id } }',
7
+ },
8
+ }));
9
+
10
+ vi.mock('../../output.js', () => ({
11
+ output: vi.fn(),
12
+ handleError: vi.fn(),
13
+ }));
14
+
15
+ vi.mock('../../validation.js', () => ({
16
+ validateBytes32: vi.fn(),
17
+ }));
18
+
19
+ import { querySchemaCommand } from '../../commands/query-schema.js';
20
+ import { graphqlQuery, QUERIES } from '../../graphql.js';
21
+ import { output, handleError } from '../../output.js';
22
+
23
+ describe('query-schema command', () => {
24
+ beforeEach(() => vi.clearAllMocks());
25
+
26
+ async function runCommand(args: string[]) {
27
+ await querySchemaCommand.parseAsync(['node', 'test', ...args]);
28
+ }
29
+
30
+ it('queries and returns schema data', async () => {
31
+ (graphqlQuery as any).mockResolvedValue({
32
+ schema: { id: '0xschema', schema: 'uint256 score', creator: '0xCreator' },
33
+ });
34
+
35
+ await runCommand(['-u', '0xschema']);
36
+
37
+ expect(graphqlQuery).toHaveBeenCalledWith('ethereum', QUERIES.getSchema, { id: '0xschema' });
38
+ expect(output).toHaveBeenCalledWith({
39
+ success: true,
40
+ data: { id: '0xschema', schema: 'uint256 score', creator: '0xCreator' },
41
+ });
42
+ });
43
+
44
+ it('throws when schema not found', async () => {
45
+ (graphqlQuery as any).mockResolvedValue({ schema: null });
46
+
47
+ await runCommand(['-u', '0xmissing']);
48
+
49
+ expect(handleError).toHaveBeenCalledWith(expect.any(Error));
50
+ const err = (handleError as any).mock.calls[0][0] as Error;
51
+ expect(err.message).toContain('not found');
52
+ });
53
+
54
+ it('uses specified chain', async () => {
55
+ (graphqlQuery as any).mockResolvedValue({
56
+ schema: { id: '0xschema' },
57
+ });
58
+
59
+ await runCommand(['-u', '0xschema', '-c', 'base']);
60
+ expect(graphqlQuery).toHaveBeenCalledWith('base', QUERIES.getSchema, { id: '0xschema' });
61
+ });
62
+ });
@@ -0,0 +1,110 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ vi.mock('../../graphql.js', () => ({
4
+ graphqlQuery: vi.fn(),
5
+ QUERIES: {
6
+ getSchemata: 'query GetSchemata',
7
+ },
8
+ }));
9
+
10
+ vi.mock('../../output.js', () => ({
11
+ output: vi.fn(),
12
+ handleError: vi.fn(),
13
+ }));
14
+
15
+ vi.mock('../../validation.js', () => ({
16
+ validateAddress: vi.fn(),
17
+ }));
18
+
19
+ import { querySchemasCommand } from '../../commands/query-schemas.js';
20
+ import { graphqlQuery, QUERIES } from '../../graphql.js';
21
+ import { output } from '../../output.js';
22
+
23
+ describe('query-schemas command', () => {
24
+ beforeEach(() => vi.clearAllMocks());
25
+
26
+ async function runCommand(args: string[]) {
27
+ await querySchemasCommand.parseAsync(['node', 'test', ...args]);
28
+ }
29
+
30
+ it('queries schemas by creator with default skip', async () => {
31
+ (graphqlQuery as any).mockResolvedValue({
32
+ schemata: [{ id: '0xs1', schema: 'uint256 x' }, { id: '0xs2', schema: 'string y' }],
33
+ });
34
+
35
+ await runCommand(['-a', '0xCreator']);
36
+
37
+ expect(graphqlQuery).toHaveBeenCalledWith('ethereum', QUERIES.getSchemata, {
38
+ creator: '0xCreator',
39
+ take: 10,
40
+ skip: 0,
41
+ });
42
+ expect(output).toHaveBeenCalledWith({
43
+ success: true,
44
+ data: { count: 2, schemas: expect.any(Array) },
45
+ });
46
+ });
47
+
48
+ it('passes custom limit', async () => {
49
+ (graphqlQuery as any).mockResolvedValue({ schemata: [] });
50
+
51
+ await runCommand(['-a', '0xCreator', '-n', '50']);
52
+
53
+ expect(graphqlQuery).toHaveBeenCalledWith(
54
+ 'ethereum',
55
+ QUERIES.getSchemata,
56
+ expect.objectContaining({ take: 50 })
57
+ );
58
+ });
59
+
60
+ it('passes skip for pagination', async () => {
61
+ (graphqlQuery as any).mockResolvedValue({ schemata: [] });
62
+
63
+ await runCommand(['-a', '0xCreator', '--skip', '15']);
64
+
65
+ expect(graphqlQuery).toHaveBeenCalledWith(
66
+ 'ethereum',
67
+ QUERIES.getSchemata,
68
+ expect.objectContaining({ skip: 15 })
69
+ );
70
+ });
71
+
72
+ it('returns empty results with count 0', async () => {
73
+ (graphqlQuery as any).mockResolvedValue({ schemata: [] });
74
+
75
+ await runCommand(['-a', '0xCreator']);
76
+
77
+ expect(output).toHaveBeenCalledWith({
78
+ success: true,
79
+ data: { count: 0, schemas: [] },
80
+ });
81
+ });
82
+
83
+ it('handles missing schemata key in response', async () => {
84
+ (graphqlQuery as any).mockResolvedValue({});
85
+
86
+ await runCommand(['-a', '0xCreator']);
87
+
88
+ expect(output).toHaveBeenCalledWith({
89
+ success: true,
90
+ data: { count: 0, schemas: [] },
91
+ });
92
+ });
93
+
94
+ it('passes GraphQL errors to handleError', async () => {
95
+ (graphqlQuery as any).mockRejectedValue(new Error('network timeout'));
96
+ await runCommand(['-a', '0xCreator']);
97
+ const { handleError } = await import('../../output.js');
98
+ expect(handleError).toHaveBeenCalledWith(expect.any(Error));
99
+ const err = (handleError as any).mock.calls[0][0] as Error;
100
+ expect(err.message).toBe('network timeout');
101
+ });
102
+
103
+ it('uses specified chain', async () => {
104
+ (graphqlQuery as any).mockResolvedValue({ schemata: [] });
105
+
106
+ await runCommand(['-a', '0xCreator', '-c', 'polygon']);
107
+
108
+ expect(graphqlQuery).toHaveBeenCalledWith('polygon', QUERIES.getSchemata, expect.any(Object));
109
+ });
110
+ });