Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/mock s3 operations #1

Merged
merged 12 commits into from
Jan 30, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ updates:
directory: '/'
schedule:
interval: 'monthly'
open-pull-requests-limit: 2
41 changes: 34 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,53 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
node-version: [18.x, 20.x]
node: [18, 20]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: pnpm/action-setup@v2.4.0
with:
version: 8
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
- uses: actions/setup-node@v4
name: Use Node.js ${{ matrix.node }}
with:
node-version: ${{ matrix.node-version }}
node-version: ${{ matrix.node }}
cache: 'pnpm'
- name: Cache pnpm modules
uses: actions/cache@v3
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Install dependencies
run: pnpm install
- name: Install dependencies and run tests with coverage
- name: Start LocalStack
uses: LocalStack/setup-localstack@main
with:
image-tag: 'latest'
install-awslocal: 'true'
configuration: |
SERVICES=s3,DEBUG=1,SKIP_SSL_CERT_DOWNLOAD=1,AWS_REGION=us-east-1,AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }},AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Check LocalStack readiness
run: |
while ! nc -z localhost 4566; do
echo "Waiting for LocalStack to be ready..."
sleep 1
done
echo "LocalStack is ready for use!"
- name: Run tests with coverage
run: pnpm run test:coverage
env:
IS_OFFLINE: true
AWS_ENDPOINT_URL: http://s3.localhost.localstack.cloud:4566
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
- uses: codecov/codecov-action@v3
name: Upload coverage reports to Codecov
with:
token: ${{ secrets.CODECOV_TOKEN }}
directory: ./coverage
files: ./cobertura-coverage.xml
files: ./coverage/cobertura-coverage.xml
fail_ci_if_error: true
verbose: true
220 changes: 200 additions & 20 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { Logging } from 'serverless/classes/Plugin'

import { getServerlessMock } from './mocks/serverless'
import {
createValidAWSInputFixture,
createValidAWSInputFixtureWithMetadata,
createValidAWSInputFixtureWithTags,
createValidDisabledInputFixture,
createValidOfflineInputFixture,
createValidOnlineInputFixture,
createValidOnlineInputFixtureWithMetadata,
createValidOnlineInputFixtureWithTags,
createValidOfflineInputFixtureWithMetadata,
createValidOfflineInputFixtureWithTags,
sampleStorage,
sampleStorageName,
} from './schemas/input.fixture'
Expand All @@ -22,15 +24,17 @@ import { mergeTags } from '../src/utils/tags'
const optionsMock = mock<Options>()
const loggingMock = mock<Logging>()

describe('SyncCloudStorage ', () => {
describe('SyncCloudStorage', () => {
beforeEach(async () => {
await setupEnvs()
})

describe('Constructor Related Tests', () => {
it('should properly configure S3 client for offline mode', async () => {
const customEndpoint = 'http://localhost:4569'
const offlineInputCustom = createValidOfflineInputFixture(customEndpoint)
const offlineInputCustom = createValidOfflineInputFixture(
'./assets/giraffe',
sampleStorageName
)
const mockServerless = getServerlessMock(offlineInputCustom, __dirname)
const syncCloudStorage = new SyncCloudStorage(
mockServerless,
Expand All @@ -40,16 +44,23 @@ describe('SyncCloudStorage ', () => {

expect(syncCloudStorage.client).toBeInstanceOf(S3Client)

if (process.env.IS_OFFLINE === 'true' && customEndpoint !== undefined) {
if (
offlineInputCustom.syncCloudStorage.offline === true &&
offlineInputCustom.syncCloudStorage.endpoint !== undefined
) {
const configuredEndpoint =
await syncCloudStorage.client?.config?.endpoint?.()

expect(customEndpoint.includes(`${configuredEndpoint?.hostname}`)).toBe(
true
)
expect(customEndpoint.includes(`${configuredEndpoint?.port}`)).toBe(
true
)
expect(
offlineInputCustom.syncCloudStorage.endpoint.includes(
`${configuredEndpoint?.hostname}`
)
).toBe(true)
expect(
offlineInputCustom.syncCloudStorage.endpoint.includes(
`${configuredEndpoint?.port}`
)
).toBe(true)
}
})

Expand All @@ -71,7 +82,7 @@ describe('SyncCloudStorage ', () => {
})

it("should not sync when there's no bucket", async () => {
const inputCustom = createValidOnlineInputFixture(
const inputCustom = createValidAWSInputFixture(
'./assets/giraffe',
sampleStorageName
)
Expand All @@ -98,9 +109,178 @@ describe('SyncCloudStorage ', () => {
})
})

describe('Storage Related Tests', () => {
describe('Storage Related Tests (Offline)', () => {
it("should throw an error when the bucket doesn't exist", async () => {
const inputCustom = createValidOfflineInputFixture(
'./assets/giraffe',
'non-existent-bucket'
)
const mockServerless = getServerlessMock(inputCustom, __dirname)
const syncCloudStorage = new SyncCloudStorage(
mockServerless,
optionsMock,
loggingMock
)

await createStorage(syncCloudStorage.getS3Client(), sampleStorage)

try {
const bucketsSpy = jest.spyOn(syncCloudStorage, 'storages')
await syncCloudStorage.storages()
expect(bucketsSpy).toHaveBeenCalledTimes(1)
} catch (error) {
expect(error).toBe(
`Error/Storage doesn't exist!: ${inputCustom.syncCloudStorage.storages[0].name}`
)
}
})

it('should sync when there are buckets', async () => {
const inputCustom = createValidOfflineInputFixture(
'./assets/giraffe',
sampleStorageName
)
const mockServerless = getServerlessMock(inputCustom, __dirname)
const syncCloudStorage = new SyncCloudStorage(
mockServerless,
optionsMock,
loggingMock
)

await createStorage(syncCloudStorage.getS3Client(), sampleStorage)

const bucketsSpy = jest.spyOn(syncCloudStorage, 'storages')
const response = await syncCloudStorage.storages()
expect(bucketsSpy).toHaveBeenCalledTimes(1)
expect(response).toMatchObject({
result: expect.arrayContaining([
expect.objectContaining({
status: expect.stringContaining('fulfilled'),
value: expect.objectContaining({
uploaded: expect.arrayContaining([
expect.objectContaining({
Bucket: expect.stringContaining(sampleStorageName),
}),
]),
}),
}),
]),
})
await deleteStorage(syncCloudStorage.getS3Client(), sampleStorage)
})

it('should sync when the bucketPrefix', async () => {
const bucketPrefix = 'animals'
const inputCustom = createValidOfflineInputFixture(
'./assets/giraffe',
sampleStorageName,
bucketPrefix
)
const mockServerless = getServerlessMock(inputCustom, __dirname)
const syncCloudStorage = new SyncCloudStorage(
mockServerless,
optionsMock,
loggingMock
)

await createStorage(syncCloudStorage.getS3Client(), sampleStorage)

const bucketsSpy = jest.spyOn(syncCloudStorage, 'storages')
const response = await syncCloudStorage.storages()
expect(bucketsSpy).toHaveBeenCalledTimes(1)
expect(response).toMatchObject({
result: expect.arrayContaining([
expect.objectContaining({
status: expect.stringContaining('fulfilled'),
value: expect.objectContaining({
uploaded: expect.arrayContaining([
expect.objectContaining({
Key: expect.stringContaining(bucketPrefix),
}),
]),
}),
}),
]),
})

for (const syncedStorage of response.result) {
if (syncedStorage.status === 'rejected') {
throw syncedStorage.reason
}

await deleteStorage(
syncCloudStorage.getS3Client(),
syncedStorage.value.storage
)
}
})

it('should sync tags', async () => {
const inputCustom = createValidOfflineInputFixtureWithTags(
'./assets/giraffe',
sampleStorageName
)
const mockServerless = getServerlessMock(inputCustom, __dirname)
const syncCloudStorage = new SyncCloudStorage(
mockServerless,
optionsMock,
loggingMock
)

await createStorage(syncCloudStorage.getS3Client(), sampleStorage)

const expectedTags = mergeTags(
[],
inputCustom.syncCloudStorage.storages[0].tags
)
const tagsSpy = jest.spyOn(syncCloudStorage, 'tags')
const newTags = await syncCloudStorage.tags()
expect(tagsSpy).toHaveBeenCalledTimes(1)

for (const { result } of newTags) {
expect(result).toBe(expectedTags)
expect(result).toBeGreaterThanOrEqual(1)

expect(
await deleteStorage(syncCloudStorage.getS3Client(), sampleStorage)
).not.toBe(undefined)
}
})

it('should sync metadata', async () => {
const inputCustom = createValidOfflineInputFixtureWithMetadata(
'./assets/giraffe',
sampleStorageName
)
const mockServerless = getServerlessMock(inputCustom, __dirname)
const syncCloudStorage = new SyncCloudStorage(
mockServerless,
optionsMock,
loggingMock
)

await createStorage(syncCloudStorage.getS3Client(), sampleStorage)

const metadataSpy = jest.spyOn(syncCloudStorage, 'metadata')
const syncedStorages = await syncCloudStorage.metadata()

expect(metadataSpy).toHaveBeenCalledTimes(1)
expect(syncedStorages).toMatchObject(
expect.arrayContaining([
expect.objectContaining({
status: expect.stringContaining('fulfilled'),
value: expect.arrayContaining([]),
}),
])
)

await deleteStorage(syncCloudStorage.getS3Client(), sampleStorage)
})
})

describe.skip('Storage Related Tests (Online)', () => {
it("should throw an error when the bucket doesn't exist", async () => {
const inputCustom = createValidOnlineInputFixture(
const inputCustom = createValidAWSInputFixture(
'./assets/giraffe',
'non-existent-bucket'
)
Expand All @@ -125,7 +305,7 @@ describe('SyncCloudStorage ', () => {
})

it('should sync when there are buckets', async () => {
const inputCustom = createValidOnlineInputFixture(
const inputCustom = createValidAWSInputFixture(
'./assets/giraffe',
sampleStorageName
)
Expand Down Expand Up @@ -160,7 +340,7 @@ describe('SyncCloudStorage ', () => {

it('should sync when the bucketPrefix', async () => {
const bucketPrefix = 'animals'
const inputCustom = createValidOnlineInputFixture(
const inputCustom = createValidAWSInputFixture(
'./assets/giraffe',
sampleStorageName,
bucketPrefix
Expand Down Expand Up @@ -205,7 +385,7 @@ describe('SyncCloudStorage ', () => {
})

it('should sync tags', async () => {
const inputCustom = createValidOnlineInputFixtureWithTags(
const inputCustom = createValidAWSInputFixtureWithTags(
'./assets/giraffe',
sampleStorageName
)
Expand Down Expand Up @@ -237,7 +417,7 @@ describe('SyncCloudStorage ', () => {
})

it('should sync metadata', async () => {
const inputCustom = createValidOnlineInputFixtureWithMetadata(
const inputCustom = createValidAWSInputFixtureWithMetadata(
'./assets/giraffe',
sampleStorageName
)
Expand Down
6 changes: 3 additions & 3 deletions test/mocks/serverless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { IServerless } from '../../src/types'
const serverlessGetProviderSpy = jest.fn().mockReturnValue({
getRegion: () => process.env.AWS_REGION ?? '',
cachedCredentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
sessionToken: process.env.AWS_SESSION_TOKEN,
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? '',
sessionToken: process.env.AWS_SESSION_TOKEN ?? '',
},
})

Expand Down
Loading
Loading