Skip to content

Commit b45414b

Browse files
test(functions): add tests for storage-confirm-upload
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5f96ef0 commit b45414b

1 file changed

Lines changed: 202 additions & 0 deletions

File tree

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { createMockContext } from '../../../tests/helpers/mock-context';
2+
3+
const mockSend = jest.fn();
4+
jest.mock('@aws-sdk/client-s3', () => ({
5+
S3Client: jest.fn().mockImplementation(() => ({
6+
send: mockSend
7+
})),
8+
HeadObjectCommand: jest.fn().mockImplementation((params) => params)
9+
}));
10+
11+
const mockPgQuery = jest.fn();
12+
const mockPgConnect = jest.fn();
13+
const mockPgEnd = jest.fn();
14+
jest.mock('pg', () => ({
15+
Client: jest.fn().mockImplementation(() => ({
16+
connect: mockPgConnect,
17+
query: mockPgQuery,
18+
end: mockPgEnd
19+
}))
20+
}));
21+
22+
const loadHandler = () => {
23+
const mod = require('../handler');
24+
return mod.default ?? mod;
25+
};
26+
27+
describe('storage-confirm-upload handler', () => {
28+
beforeEach(() => {
29+
jest.resetModules();
30+
jest.clearAllMocks();
31+
process.env.S3_ENDPOINT = 'http://localhost:9000';
32+
process.env.AWS_ACCESS_KEY_ID = 'minioadmin';
33+
process.env.AWS_SECRET_ACCESS_KEY = 'minioadmin';
34+
process.env.AWS_REGION = 'us-east-1';
35+
});
36+
37+
afterEach(() => {
38+
delete process.env.S3_ENDPOINT;
39+
delete process.env.AWS_ACCESS_KEY_ID;
40+
delete process.env.AWS_SECRET_ACCESS_KEY;
41+
delete process.env.AWS_REGION;
42+
});
43+
44+
describe('validation', () => {
45+
it('throws on missing file_id', async () => {
46+
const handler = loadHandler();
47+
await expect(
48+
handler(
49+
{ key: 'test.txt', bucket_id: 'bucket-123' },
50+
createMockContext()
51+
)
52+
).rejects.toThrow('Missing required fields');
53+
});
54+
55+
it('throws on missing key', async () => {
56+
const handler = loadHandler();
57+
await expect(
58+
handler(
59+
{ file_id: '123', bucket_id: 'bucket-123' },
60+
createMockContext()
61+
)
62+
).rejects.toThrow('Missing required fields');
63+
});
64+
65+
it('throws on missing bucket_id', async () => {
66+
const handler = loadHandler();
67+
await expect(
68+
handler(
69+
{ file_id: '123', key: 'test.txt' },
70+
createMockContext()
71+
)
72+
).rejects.toThrow('Missing required fields');
73+
});
74+
});
75+
76+
describe('S3 file check', () => {
77+
it('throws when file not found in S3', async () => {
78+
const handler = loadHandler();
79+
const notFoundError = new Error('Not Found');
80+
(notFoundError as any).name = 'NotFound';
81+
mockSend.mockRejectedValueOnce(notFoundError);
82+
83+
await expect(
84+
handler(
85+
{ file_id: '123', key: 'test.txt', bucket_id: 'bucket-123' },
86+
createMockContext()
87+
)
88+
).rejects.toThrow('File not found in S3');
89+
});
90+
91+
it('rethrows unexpected S3 errors', async () => {
92+
const handler = loadHandler();
93+
mockSend.mockRejectedValueOnce(new Error('Network error'));
94+
95+
await expect(
96+
handler(
97+
{ file_id: '123', key: 'test.txt', bucket_id: 'bucket-123' },
98+
createMockContext()
99+
)
100+
).rejects.toThrow('Network error');
101+
});
102+
});
103+
104+
describe('confirm upload', () => {
105+
it('calls confirm function when file exists in S3', async () => {
106+
const handler = loadHandler();
107+
mockSend.mockResolvedValueOnce({}); // HeadObject success
108+
mockPgQuery.mockResolvedValueOnce({ rows: [] }); // confirm_uploaded call
109+
110+
const result = await handler(
111+
{
112+
file_id: '123',
113+
key: 'test.txt',
114+
bucket_id: 'bucket-123',
115+
schema: 'test-db-app-public',
116+
table: 'app_files'
117+
},
118+
createMockContext()
119+
);
120+
121+
expect(result.success).toBe(true);
122+
expect(result.file_id).toBe('123');
123+
expect(mockPgQuery).toHaveBeenCalled();
124+
});
125+
126+
it('uses default schema and table when not provided', async () => {
127+
const handler = loadHandler();
128+
mockSend.mockResolvedValueOnce({}); // HeadObject success
129+
mockPgQuery.mockResolvedValueOnce({ rows: [] }); // confirm_uploaded call
130+
131+
const result = await handler(
132+
{
133+
file_id: '123',
134+
key: 'test.txt',
135+
bucket_id: 'bucket-123'
136+
},
137+
createMockContext()
138+
);
139+
140+
expect(result.success).toBe(true);
141+
// Should use default storage_public and app_files
142+
expect(mockPgQuery).toHaveBeenCalledWith(
143+
expect.stringContaining('app_files_confirm_uploaded'),
144+
['123']
145+
);
146+
});
147+
});
148+
149+
describe('bucket name resolution', () => {
150+
it('resolves bucket name from database when schema and databaseId provided', async () => {
151+
const handler = loadHandler();
152+
// First query: resolve bucket name
153+
mockPgQuery.mockResolvedValueOnce({ rows: [{ type: 'public' }] });
154+
// HeadObject success
155+
mockSend.mockResolvedValueOnce({});
156+
// Second query: confirm_uploaded call
157+
mockPgQuery.mockResolvedValueOnce({ rows: [] });
158+
159+
const context = createMockContext();
160+
context.job.databaseId = 'db-123';
161+
162+
const result = await handler(
163+
{
164+
file_id: '123',
165+
key: 'test.txt',
166+
bucket_id: 'bucket-123',
167+
schema: 'test-db-app-public'
168+
},
169+
context
170+
);
171+
172+
expect(result.success).toBe(true);
173+
expect(result.bucket).toBe('test-bucket-public-db-123');
174+
});
175+
176+
it('falls back to bucket_id when resolution fails', async () => {
177+
const handler = loadHandler();
178+
// First query fails
179+
mockPgQuery.mockRejectedValueOnce(new Error('DB error'));
180+
// HeadObject success
181+
mockSend.mockResolvedValueOnce({});
182+
// Second query: confirm_uploaded call
183+
mockPgQuery.mockResolvedValueOnce({ rows: [] });
184+
185+
const context = createMockContext();
186+
context.job.databaseId = 'db-123';
187+
188+
const result = await handler(
189+
{
190+
file_id: '123',
191+
key: 'test.txt',
192+
bucket_id: 'bucket-123',
193+
schema: 'test-db-app-public'
194+
},
195+
context
196+
);
197+
198+
expect(result.success).toBe(true);
199+
expect(result.bucket).toBe('bucket-123');
200+
});
201+
});
202+
});

0 commit comments

Comments
 (0)