1import { describe, it, expect, vi, beforeEach } from 'vitest';
2import { ATProtocolNotary } from '../../src/lib/notary';
3
4// Mock external dependencies
5vi.mock('@atproto/api', () => ({
6 AtpAgent: vi.fn().mockImplementation(() => ({
7 com: {
8 atproto: {
9 repo: {
10 getRecord: vi.fn().mockResolvedValue({
11 data: {
12 value: { text: 'Test post', createdAt: '2024-01-01' },
13 cid: 'bafyreiabc123',
14 },
15 }),
16 },
17 },
18 },
19 })),
20}));
21
22vi.mock('@ethereum-attestation-service/eas-sdk', () => {
23 const mockEAS = {
24 connect: vi.fn(),
25 attest: vi.fn().mockResolvedValue({
26 wait: vi.fn().mockResolvedValue('0xattestationuid123'),
27 receipt: {
28 hash: '0xtxhash123',
29 transactionHash: '0xtxhash123',
30 },
31 }),
32 getAttestation: vi.fn().mockResolvedValue({
33 uid: '0xattestationuid123',
34 attester: '0xattester',
35 revocationTime: 0n,
36 data: '0xencodeddata',
37 }),
38 };
39
40 const mockSchemaRegistry = {
41 connect: vi.fn(),
42 register: vi.fn().mockResolvedValue({
43 wait: vi.fn().mockResolvedValue({
44 transactionHash: '0xschemahash',
45 hash: '0xschemahash',
46 }),
47 receipt: {
48 hash: '0xschemahash',
49 transactionHash: '0xschemahash',
50 },
51 }),
52 };
53
54 return {
55 default: {
56 EAS: vi.fn(() => mockEAS),
57 SchemaRegistry: vi.fn(() => mockSchemaRegistry),
58 SchemaEncoder: vi.fn().mockImplementation(() => ({
59 encodeData: vi.fn().mockReturnValue('0xencodeddata'),
60 decodeData: vi.fn().mockReturnValue([
61 { name: 'recordURI', value: { value: 'at://did:plc:test/app.bsky.feed.post/123' } },
62 { name: 'cid', value: { value: 'bafyreiabc123' } },
63 { name: 'contentHash', value: { value: '0xa64b286ffd5cc55c57f4e9c74d1122aa081dc5f662648cb5cc5ced74e0e12cd5' } },
64 { name: 'pds', value: { value: 'https://pds.example.com' } },
65 { name: 'timestamp', value: { value: 1234567890 } },
66 ]),
67 })),
68 NO_EXPIRATION: 0n,
69 },
70 };
71});
72
73global.fetch = vi.fn();
74
75describe('ATProtocolNotary', () => {
76 beforeEach(() => {
77 vi.clearAllMocks();
78
79 // Mock DID resolution
80 (global.fetch as any).mockResolvedValue({
81 ok: true,
82 json: async () => ({
83 service: [
84 {
85 id: '#atproto_pds',
86 type: 'AtprotoPersonalDataServer',
87 serviceEndpoint: 'https://pds.example.com',
88 },
89 ],
90 }),
91 });
92 });
93
94 describe('constructor', () => {
95 it('should create instance with valid config', () => {
96 const notary = new ATProtocolNotary({
97 privateKey: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
98 schemaUID: '0xschemauid',
99 }, 'sepolia');
100
101 expect(notary).toBeInstanceOf(ATProtocolNotary);
102 expect(notary.getAddress()).toBeTruthy();
103 });
104 });
105
106 describe('resolveDIDtoPDS', () => {
107 it('should resolve did:plc to PDS', async () => {
108 const notary = new ATProtocolNotary({
109 schemaUID: '0xschemauid',
110 });
111
112 const pds = await notary.resolveDIDtoPDS('did:plc:test123');
113
114 expect(pds).toBe('https://pds.example.com');
115 expect(global.fetch).toHaveBeenCalledWith('https://plc.directory/did:plc:test123');
116 });
117
118 it('should throw error for unsupported DID method', async () => {
119 const notary = new ATProtocolNotary({
120 schemaUID: '0xschemauid',
121 });
122
123 await expect(notary.resolveDIDtoPDS('did:unsupported:test')).rejects.toThrow(
124 'Unsupported DID method'
125 );
126 });
127
128 it('should throw error when PDS not found', async () => {
129 (global.fetch as any).mockResolvedValueOnce({
130 ok: true,
131 json: async () => ({ service: [] }),
132 });
133
134 const notary = new ATProtocolNotary({
135 schemaUID: '0xschemauid',
136 });
137
138 await expect(notary.resolveDIDtoPDS('did:plc:test')).rejects.toThrow(
139 'No PDS endpoint found'
140 );
141 });
142 });
143
144 describe('fetchRecord', () => {
145 it('should fetch record from PDS', async () => {
146 const notary = new ATProtocolNotary({
147 schemaUID: '0xschemauid',
148 });
149
150 const result = await notary.fetchRecord('at://did:plc:test/app.bsky.feed.post/123');
151
152 expect(result.record.value.text).toBe('Test post');
153 expect(result.record.cid).toBe('bafyreiabc123');
154 expect(result.pds).toBe('https://pds.example.com');
155 });
156 });
157
158 describe('notarizeRecord', () => {
159 it('should create attestation successfully', async () => {
160 const notary = new ATProtocolNotary({
161 privateKey: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
162 schemaUID: '0xschemauid123',
163 });
164
165 const result = await notary.notarizeRecord('at://did:plc:test/app.bsky.feed.post/123');
166
167 expect(result.attestationUID).toBe('0xattestationuid123');
168 expect(result.recordURI).toBe('at://did:plc:test/app.bsky.feed.post/123');
169 expect(result.cid).toBe('bafyreiabc123');
170 expect(result.pds).toBe('https://pds.example.com');
171 expect(result.transactionHash).toBeTruthy();
172 });
173 });
174
175 describe('verifyAttestation', () => {
176 it('should verify attestation successfully', async () => {
177 const notary = new ATProtocolNotary({
178 schemaUID: '0xschemauid',
179 });
180
181 const result = await notary.verifyAttestation('0xattestationuid123');
182
183 expect(result.uid).toBe('0xattestationuid123');
184 expect(result.recordURI).toBe('at://did:plc:test/app.bsky.feed.post/123');
185 expect(result.cid).toBe('bafyreiabc123');
186 expect(result.pds).toBe('https://pds.example.com');
187 expect(result.attester).toBe('0xattester');
188 expect(result.revoked).toBe(false);
189 });
190 });
191
192 describe('compareWithCurrent', () => {
193 it('should detect unchanged content', async () => {
194 const notary = new ATProtocolNotary({
195 schemaUID: '0xschemauid',
196 });
197
198 const attestation = await notary.verifyAttestation('0xattestationuid123');
199 const comparison = await notary.compareWithCurrent(attestation);
200
201 expect(comparison.exists).toBe(true);
202 expect(comparison.cidMatches).toBe(true);
203 expect(comparison.hashMatches).toBe(true);
204 expect(comparison.currentCid).toBeTruthy();
205 expect(comparison.currentHash).toBeTruthy();
206 });
207
208 it('should detect changed content', async () => {
209 // Mock different CID and hash
210 const { AtpAgent } = await import('@atproto/api');
211 (AtpAgent as any).mockImplementation(() => ({
212 com: {
213 atproto: {
214 repo: {
215 getRecord: vi.fn().mockResolvedValue({
216 data: {
217 value: { text: 'Modified post', createdAt: '2024-01-02' },
218 cid: 'bafyreidifferent123',
219 },
220 }),
221 },
222 },
223 },
224 }));
225
226 const notary = new ATProtocolNotary({
227 schemaUID: '0xschemauid',
228 });
229
230 const attestation = await notary.verifyAttestation('0xattestationuid123');
231 const comparison = await notary.compareWithCurrent(attestation);
232
233 expect(comparison.exists).toBe(true);
234 expect(comparison.cidMatches).toBe(false);
235 expect(comparison.hashMatches).toBe(false);
236 });
237 });
238
239});