Skip to content

Commit 4984ec2

Browse files
authored
feat: storage client (#12)
* feat: storage client * minor fixes & code clean up * tests * fix: ensure you cant init client that has been initialized already * refactor: update client structure * fix logs and dont fail if start func is called more than once
1 parent 1f43f45 commit 4984ec2

File tree

13 files changed

+823
-74
lines changed

13 files changed

+823
-74
lines changed

.github/workflows/release.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ jobs:
4242
- run: pnpm build
4343
if: ${{ steps.release.outputs.release_created }}
4444

45+
- run: pnpm test
46+
if: ${{ steps.release.outputs.release_created }}
47+
4548
- run: pnpm publish --no-git-checks --access=public --provenance
4649
if: ${{ steps.release.outputs.release_created }}
4750
env:

__tests__/actions/retrieve.test.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import { retrieveAction } from '../../src/actions/retrieve';
3+
import { validateStorageClientConfig } from '../../src/environments';
4+
import { defaultGatewayUrl, getCIDsFromMessage } from '../../src/utils';
5+
6+
// Mock dependencies
7+
vi.mock('@elizaos/core', () => ({
8+
elizaLogger: {
9+
log: vi.fn(),
10+
error: vi.fn(),
11+
info: vi.fn()
12+
}
13+
}));
14+
15+
vi.mock('../../src/environments', () => ({
16+
validateStorageClientConfig: vi.fn().mockResolvedValue({
17+
GATEWAY_URL: 'https://mock-gateway.link'
18+
})
19+
}));
20+
21+
vi.mock('../../src/utils', () => ({
22+
defaultGatewayUrl: 'https://w3s.link',
23+
getCIDsFromMessage: vi.fn()
24+
}));
25+
26+
describe('retrieveAction', () => {
27+
let mockRuntime: any;
28+
let mockCallback: any;
29+
30+
beforeEach(() => {
31+
mockRuntime = {
32+
getParameter: vi.fn()
33+
};
34+
mockCallback = vi.fn();
35+
36+
vi.clearAllMocks();
37+
});
38+
39+
describe('validate', () => {
40+
it('should return true when validation passes', async () => {
41+
const mockMessage = { content: { text: 'Test message' } };
42+
43+
const result = await retrieveAction.validate(mockRuntime, mockMessage as any);
44+
45+
expect(result).toBe(true);
46+
expect(validateStorageClientConfig).toHaveBeenCalledWith(mockRuntime);
47+
});
48+
});
49+
50+
describe('handler', () => {
51+
it('should return false when no CIDs are found in the message', async () => {
52+
const mockMessage = { content: { text: 'No CIDs here' } };
53+
(getCIDsFromMessage as any).mockReturnValue([]);
54+
55+
const result = await retrieveAction.handler(
56+
mockRuntime,
57+
mockMessage as any,
58+
{} as any,
59+
{},
60+
mockCallback
61+
);
62+
63+
expect(result).toBe(false);
64+
expect(getCIDsFromMessage).toHaveBeenCalledWith(mockMessage);
65+
expect(mockCallback).toHaveBeenCalledWith({
66+
text: "You didn't provide any CIDs to retrieve the content."
67+
});
68+
});
69+
70+
it('should return true and generate download links when CIDs are found', async () => {
71+
const mockMessage = { content: { text: 'Get file with CID QmTest123 and bafyTest456' } };
72+
const mockCIDs = ['QmTest123', 'bafyTest456'];
73+
(getCIDsFromMessage as any).mockReturnValue(mockCIDs);
74+
75+
const result = await retrieveAction.handler(
76+
mockRuntime,
77+
mockMessage as any,
78+
{} as any,
79+
{},
80+
mockCallback
81+
);
82+
83+
expect(result).toBe(true);
84+
expect(getCIDsFromMessage).toHaveBeenCalledWith(mockMessage);
85+
expect(mockCallback).toHaveBeenCalledWith({
86+
text: expect.stringContaining('https://mock-gateway.link/ipfs/QmTest123')
87+
});
88+
expect(mockCallback).toHaveBeenCalledWith({
89+
text: expect.stringContaining('https://mock-gateway.link/ipfs/bafyTest456')
90+
});
91+
});
92+
93+
it('should use default gateway URL when not provided in config', async () => {
94+
const mockMessage = { content: { text: 'Get file with CID QmTest123' } };
95+
const mockCIDs = ['QmTest123'];
96+
(getCIDsFromMessage as any).mockReturnValue(mockCIDs);
97+
(validateStorageClientConfig as any).mockResolvedValue({});
98+
99+
const result = await retrieveAction.handler(
100+
mockRuntime,
101+
mockMessage as any,
102+
{} as any,
103+
{},
104+
mockCallback
105+
);
106+
107+
expect(result).toBe(true);
108+
expect(mockCallback).toHaveBeenCalledWith({
109+
text: expect.stringContaining(`${defaultGatewayUrl}/ipfs/QmTest123`)
110+
});
111+
});
112+
113+
it('should handle errors properly', async () => {
114+
const mockMessage = { content: { text: 'Get file with CID QmTest123' } };
115+
const mockCIDs = ['QmTest123'];
116+
(getCIDsFromMessage as any).mockReturnValue(mockCIDs);
117+
118+
(validateStorageClientConfig as any).mockResolvedValue({
119+
GATEWAY_URL: 'https://mock-gateway.link'
120+
});
121+
122+
mockCallback.mockImplementationOnce(() => {
123+
throw new Error('Simulated error during callback');
124+
});
125+
126+
const result = await retrieveAction.handler(
127+
mockRuntime,
128+
mockMessage as any,
129+
{} as any,
130+
{},
131+
mockCallback
132+
);
133+
134+
expect(result).toBe(false);
135+
expect(mockCallback).toHaveBeenCalledTimes(2);
136+
expect(mockCallback).toHaveBeenNthCalledWith(2, {
137+
text: expect.stringContaining('Simulated error during callback')
138+
});
139+
});
140+
});
141+
});

__tests__/actions/upload.test.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { uploadAction } from '../../src/actions/upload';
3+
import { validateStorageClientConfig } from '../../src/environments';
4+
import { createStorageClient } from '../../src/clients/storage';
5+
import fs from 'fs';
6+
7+
// Mock dependencies
8+
vi.mock('fs', () => ({
9+
default: {
10+
readFileSync: vi.fn().mockReturnValue(Buffer.from('test file content')),
11+
promises: {
12+
readFile: vi.fn().mockResolvedValue(Buffer.from('test file content')),
13+
stat: vi.fn().mockResolvedValue({
14+
isFile: () => true,
15+
size: 1024
16+
})
17+
}
18+
}
19+
}));
20+
21+
vi.mock('@elizaos/core', () => ({
22+
elizaLogger: {
23+
log: vi.fn(),
24+
error: vi.fn(),
25+
info: vi.fn(),
26+
success: vi.fn()
27+
}
28+
}));
29+
30+
vi.mock('../../src/environments', () => ({
31+
validateStorageClientConfig: vi.fn().mockResolvedValue({
32+
GATEWAY_URL: 'https://mock-gateway.link'
33+
})
34+
}));
35+
36+
vi.mock('../../src/clients/storage', () => {
37+
const mockPut = vi.fn().mockResolvedValue({
38+
car: { cid: 'mock-car-cid' },
39+
root: 'mock-root-cid'
40+
});
41+
42+
return {
43+
createStorageClient: vi.fn().mockResolvedValue({
44+
upload: vi.fn().mockReturnValue({
45+
put: mockPut
46+
})
47+
})
48+
};
49+
});
50+
51+
describe('uploadAction', () => {
52+
let mockRuntime: any;
53+
let mockCallback: any;
54+
55+
beforeEach(() => {
56+
mockRuntime = {
57+
getParameter: vi.fn()
58+
};
59+
mockCallback = vi.fn();
60+
61+
vi.clearAllMocks();
62+
});
63+
64+
describe('validate', () => {
65+
it('should return true when validation passes', async () => {
66+
const mockMessage = { content: { text: 'Test message' } };
67+
const result = await uploadAction.validate(mockRuntime, mockMessage as any);
68+
69+
expect(result).toBe(true);
70+
expect(validateStorageClientConfig).toHaveBeenCalledWith(mockRuntime);
71+
});
72+
});
73+
74+
describe('handler', () => {
75+
it('should return false when no attachments are provided', async () => {
76+
const mockMessage = { content: { attachments: [] } };
77+
78+
const result = await uploadAction.handler(
79+
mockRuntime,
80+
mockMessage as any,
81+
{} as any,
82+
{},
83+
mockCallback
84+
);
85+
86+
expect(result).toBe(false);
87+
expect(mockCallback).toHaveBeenCalledWith({
88+
text: "Looks like you didn't attach any files. Please attach a file and try again.",
89+
action: null
90+
});
91+
});
92+
93+
it('should handle file uploads when attachments are provided', async () => {
94+
const mockAttachments = [
95+
{ url: 'file:///path/to/file1.txt', title: 'file1.txt' },
96+
{ url: 'file:///path/to/file2.jpg', title: 'file2.jpg' }
97+
];
98+
const mockMessage = { content: { attachments: mockAttachments } };
99+
100+
const result = await uploadAction.handler(
101+
mockRuntime,
102+
mockMessage as any,
103+
{} as any,
104+
{},
105+
mockCallback
106+
);
107+
108+
expect(mockCallback).toHaveBeenNthCalledWith(1, {
109+
text: "Sure thing! Starting the engines, hold on tight. Uploading file(s) to Storacha...",
110+
action: null
111+
});
112+
expect(validateStorageClientConfig).toHaveBeenCalled();
113+
expect(createStorageClient).toHaveBeenCalled();
114+
expect(fs.readFileSync).toHaveBeenCalled();
115+
});
116+
117+
it('should handle errors from reading non-file URLs', async () => {
118+
const mockAttachments = [
119+
{ url: 'https://example.com/image.jpg', title: 'web-image.jpg' }
120+
];
121+
const mockMessage = { content: { attachments: mockAttachments } };
122+
123+
(fs.readFileSync as any).mockImplementationOnce(() => {
124+
throw new Error('default.readFileSync is not a function');
125+
});
126+
127+
const result = await uploadAction.handler(
128+
mockRuntime,
129+
mockMessage as any,
130+
{} as any,
131+
{},
132+
mockCallback
133+
);
134+
135+
expect(mockCallback).toHaveBeenNthCalledWith(1, {
136+
text: "Sure thing! Starting the engines, hold on tight. Uploading file(s) to Storacha...",
137+
action: null
138+
});
139+
expect(mockCallback).toHaveBeenNthCalledWith(2, {
140+
text: "I'm sorry, I couldn't upload the file(s) to Storacha. Please try again later.",
141+
content: { error: 'default.readFileSync is not a function' }
142+
});
143+
});
144+
145+
it('should handle errors during client initialization', async () => {
146+
const mockAttachments = [
147+
{ url: 'file:///path/to/file.txt', title: 'file.txt' }
148+
];
149+
const mockMessage = { content: { attachments: mockAttachments } };
150+
const mockError = new Error('Upload failed');
151+
(createStorageClient as any).mockRejectedValueOnce(mockError);
152+
153+
const result = await uploadAction.handler(
154+
mockRuntime,
155+
mockMessage as any,
156+
{} as any,
157+
{},
158+
mockCallback
159+
);
160+
161+
expect(result).toBe(false);
162+
expect(mockCallback).toHaveBeenNthCalledWith(1, {
163+
text: "Sure thing! Starting the engines, hold on tight. Uploading file(s) to Storacha...",
164+
action: null
165+
});
166+
expect(mockCallback).toHaveBeenNthCalledWith(2, {
167+
text: "I'm sorry, I couldn't upload the file(s) to Storacha. Please try again later.",
168+
content: { error: 'Upload failed' }
169+
});
170+
});
171+
});
172+
});

0 commit comments

Comments
 (0)