Skip to content

Commit 71c70a3

Browse files
authored
feat(condo): added aws file adapter (#5819)
1 parent 4b6c6d7 commit 71c70a3

File tree

5 files changed

+566
-1
lines changed

5 files changed

+566
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
const index = require('@app/condo/index')
5+
const AWS = require('aws-sdk')
6+
7+
const { awsRouterHandler } = require('@open-condo/keystone/fileAdapter/awsFileAdapter')
8+
const {
9+
setFakeClientMode,
10+
makeLoggedInAdminClient,
11+
} = require('@open-condo/keystone/test.utils')
12+
13+
const {
14+
createTestBillingIntegration,
15+
createTestBillingIntegrationOrganizationContext,
16+
} = require('@condo/domains/billing/utils/testSchema')
17+
const { makeClientWithProperty } = require('@condo/domains/property/utils/testSchema')
18+
const { makeClientWithSupportUser } = require('@condo/domains/user/utils/testSchema')
19+
20+
21+
const { keystone } = index
22+
23+
const FOLDER_NAME = '__jest_test_api___'
24+
25+
26+
class AwsTest {
27+
constructor (config) {
28+
this.bucket = config.bucket
29+
if (!this.bucket) {
30+
throw new Error('AwsAdapter: S3Adapter requires a bucket name.')
31+
}
32+
this.s3 = new AWS.S3(config.s3Options)
33+
this.folder = config.folder
34+
}
35+
36+
async checkBucket () {
37+
try {
38+
await this.s3.headBucket({
39+
Bucket: this.bucket,
40+
}).promise()
41+
return true
42+
} catch (error) {
43+
return false
44+
}
45+
}
46+
47+
async uploadObject (name, text) {
48+
const serverAnswer = await this.s3.upload({
49+
Bucket: this.bucket,
50+
Key: `${this.folder}/${name}`,
51+
Body: text,
52+
}).promise()
53+
return serverAnswer
54+
}
55+
56+
async checkObjectExists (name) {
57+
try {
58+
const serverAnswer = await this.s3.headObject({
59+
Bucket: this.bucket,
60+
Key: `${this.folder}/${name}`,
61+
}).promise()
62+
return serverAnswer
63+
} catch (error){
64+
if (error.statusCode === 404) {
65+
return null
66+
}
67+
return error
68+
69+
}
70+
}
71+
72+
async deleteObject (name) {
73+
const serverAnswer = await this.s3.deleteObject({
74+
Bucket: this.bucket,
75+
Key: `${this.folder}/${name}`,
76+
}).promise()
77+
return serverAnswer
78+
}
79+
80+
async getMeta (name) {
81+
const result = await this.s3.headObject({
82+
Bucket: this.bucket,
83+
Key: `${this.folder}/${name}`,
84+
}).promise()
85+
86+
if (result?.Metadata) {
87+
return result.Metadata
88+
} else {
89+
return {}
90+
}
91+
}
92+
93+
async setMeta (name, newMeta = {}) {
94+
try {
95+
await this.s3.copyObject({
96+
Bucket: this.bucket,
97+
CopySource: `${this.bucket}/${this.folder}/${name}`,
98+
Key: `${this.folder}/${name}`,
99+
Metadata: newMeta,
100+
MetadataDirective: 'REPLACE',
101+
}).promise()
102+
return true
103+
} catch (err) {
104+
return false
105+
}
106+
}
107+
108+
109+
static async initApi () {
110+
const S3Config = {
111+
...(process.env.AWS_CONFIG ? JSON.parse(process.env.AWS_CONFIG) : {}),
112+
folder: FOLDER_NAME,
113+
}
114+
if (!S3Config.bucket) {
115+
console.warn('Aws Api: invalid configuration')
116+
return null
117+
}
118+
const Api = new AwsTest(S3Config)
119+
const check = await Api.checkBucket()
120+
if (!check) {
121+
console.warn(`Aws Api: no access to bucket ${Api.bucket}`)
122+
return null
123+
}
124+
return Api
125+
}
126+
}
127+
128+
describe('Aws', () => {
129+
let handler
130+
let mockedNext, mockedReq, mockedRes
131+
132+
setFakeClientMode(index)
133+
134+
beforeAll(async () => {
135+
handler = awsRouterHandler({ keystone })
136+
mockedNext = () => {
137+
throw new Error('calling method not expected by test cases')
138+
}
139+
mockedReq = (file, user) => ({
140+
get: (header) => header,
141+
params: { file },
142+
user,
143+
})
144+
mockedRes = {
145+
sendStatus: mockedNext,
146+
status: mockedNext,
147+
end: mockedNext,
148+
json: mockedNext,
149+
redirect: mockedNext,
150+
}
151+
})
152+
153+
describe('Aws SDK', () => {
154+
it('can add file to s3', async () => {
155+
const Api = await AwsTest.initApi()
156+
if (Api) {
157+
const name = `testFile_${Math.random()}.txt`
158+
const uploadedObject = await Api.uploadObject(name, `Random text ${Math.random()}`)
159+
160+
expect(uploadedObject?.ETag).toBeDefined()
161+
expect(uploadedObject?.ETag?.length).toBeGreaterThan(0)
162+
expect(uploadedObject?.ServerSideEncryption).toBeDefined()
163+
expect(uploadedObject?.ServerSideEncryption?.length).toBeGreaterThan(0)
164+
165+
const receivedObject = await Api.checkObjectExists(name)
166+
167+
expect(receivedObject?.ETag).toBeDefined()
168+
expect(receivedObject?.ETag?.length).toBeGreaterThan(0)
169+
expect(receivedObject?.ETag).toEqual(uploadedObject?.ETag)
170+
expect(receivedObject?.ServerSideEncryption).toBeDefined()
171+
expect(receivedObject?.ServerSideEncryption?.length).toBeGreaterThan(0)
172+
expect(receivedObject?.ServerSideEncryption).toEqual(uploadedObject?.ServerSideEncryption)
173+
174+
const deleteResponse = await Api.deleteObject(name)
175+
expect(deleteResponse).toEqual({})
176+
177+
const objectAfterDeletion = await Api.checkObjectExists(name)
178+
expect(objectAfterDeletion).toBeNull()
179+
}
180+
})
181+
it('can set meta to a file', async () => {
182+
const Api = await AwsTest.initApi()
183+
if (Api) {
184+
const name = `testFile_${Math.random()}.txt`
185+
await Api.uploadObject(name, `Random text ${Math.random()}`)
186+
const setMetaResult = await Api.setMeta(name, { listkey: 'Some listkey', id: name })
187+
expect(setMetaResult).toBe(true)
188+
const meta = await Api.getMeta(name)
189+
expect(meta.listkey).toBe('Some listkey')
190+
expect(meta.id).toBe(name)
191+
await Api.deleteObject(name)
192+
}
193+
})
194+
it('can delete file from s3', async () => {
195+
const Api = await AwsTest.initApi()
196+
if (Api) {
197+
const name = `testFile_${Math.random()}.txt`
198+
const uploadedObject = await Api.uploadObject(name, `Random text ${Math.random()}`)
199+
expect(uploadedObject?.ETag).toBeDefined()
200+
expect(uploadedObject?.ETag?.length).toBeGreaterThan(0)
201+
expect(uploadedObject?.ServerSideEncryption).toBeDefined()
202+
expect(uploadedObject?.ServerSideEncryption?.length).toBeGreaterThan(0)
203+
204+
const deleteResponse = await Api.deleteObject(name)
205+
expect(deleteResponse).toEqual({})
206+
207+
const objectAfterDeletion = await Api.checkObjectExists(name)
208+
expect(objectAfterDeletion).toBeNull()
209+
}
210+
})
211+
212+
})
213+
describe('Check access to read file', () => {
214+
let userClient, support, adminClient,
215+
integration, billingContext, Api,
216+
getFileWithMeta
217+
beforeAll(async () => {
218+
const Api = await AwsTest.initApi()
219+
if (Api) {
220+
userClient = await makeClientWithProperty()
221+
support = await makeClientWithSupportUser()
222+
adminClient = await makeLoggedInAdminClient()
223+
224+
integration = (await createTestBillingIntegration(support))[0]
225+
billingContext = (await createTestBillingIntegrationOrganizationContext(userClient, userClient.organization, integration))[0]
226+
}
227+
getFileWithMeta = async (meta) => {
228+
const name = `testFile_${Math.random()}.txt` // NOSONAR
229+
const objectName = `${FOLDER_NAME}/${name}`
230+
await Api.uploadObject(name, `Random text ${Math.random()}`) // NOSONAR
231+
const setMetaResult = await Api.setMeta(name, meta)
232+
expect(setMetaResult).toBe(true)
233+
234+
return {
235+
name, objectName,
236+
}
237+
}
238+
239+
})
240+
it('check access for read file by model', async () => {
241+
if (Api) {
242+
const { objectName } = await getFileWithMeta({
243+
listkey: 'BillingIntegrationOrganizationContext',
244+
id: billingContext.id,
245+
})
246+
247+
handler(
248+
mockedReq(objectName, adminClient.user),
249+
{ ...mockedRes, redirect: console.log },
250+
mockedNext,
251+
)
252+
}
253+
})
254+
it('check access for read file by model param', async () => {
255+
if (Api) {
256+
const { objectName } = await getFileWithMeta({
257+
listkey: 'BillingIntegrationOrganizationContext',
258+
id: billingContext.id,
259+
propertyquery: 'organization { id }',
260+
propertyvalue: userClient.organization.id,
261+
})
262+
263+
handler(
264+
mockedReq(objectName, adminClient.user),
265+
{ ...mockedRes, redirect: console.log },
266+
mockedNext,
267+
)
268+
}
269+
})
270+
271+
})
272+
})

apps/condo/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
"amplitude-js": "^8.18.1",
8989
"antd": "^4.24.12",
9090
"antd-mask-input": "^0.1.15",
91+
"aws-sdk": "^2.1692.0",
9192
"axios": "^0.26.0",
9293
"big.js": "^6.1.1",
9394
"body-parser": "^1.19.1",

0 commit comments

Comments
 (0)