// Copyright (c) 2018, Compiler Explorer Authors // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. import {Readable} from 'stream'; import {DynamoDB, GetItemCommand, PutItemCommand, QueryCommand, UpdateItemCommand} from '@aws-sdk/client-dynamodb'; import {GetObjectCommand, PutObjectCommand, S3} from '@aws-sdk/client-s3'; import {sdkStreamMixin} from '@smithy/util-stream'; import {mockClient} from 'aws-sdk-client-mock'; import {beforeEach, describe, expect, it} from 'vitest'; import * as properties from '../../lib/properties.js'; import {StorageS3} from '../../lib/storage/index.js'; describe('Find unique subhash tests', () => { const mockDynamoDb = mockClient(DynamoDB); const mockS3 = mockClient(S3); beforeEach(() => { mockDynamoDb.reset(); mockS3.reset(); }); const compilerProps = properties.fakeProps({}); const httpRootDir = '/'; const awsProps = properties.fakeProps({ region: 'not-a-region', storageBucket: 'bucket', storagePrefix: 'prefix', storageDynamoTable: 'table', }); it('works when empty', async () => { const storage = new StorageS3(httpRootDir, compilerProps, awsProps); mockDynamoDb.on(QueryCommand, {TableName: 'table'}).resolves({}); await expect(storage.findUniqueSubhash('ABCDEFGHIJKLMNOPQRSTUV')).resolves.toEqual({ alreadyPresent: false, prefix: 'ABCDEF', uniqueSubHash: 'ABCDEFGHI', }); }); it('works when not empty', async () => { const storage = new StorageS3(httpRootDir, compilerProps, awsProps); mockDynamoDb.on(QueryCommand, {TableName: 'table'}).resolves({ Items: [ { full_hash: {S: 'ZZVZT'}, unique_subhash: {S: 'ZZVZT'}, }, ], }); await expect(storage.findUniqueSubhash('ABCDEFGHIJKLMNOPQRSTUV')).resolves.toEqual({ alreadyPresent: false, prefix: 'ABCDEF', uniqueSubHash: 'ABCDEFGHI', }); }); it("works when there's a collision", async () => { const storage = new StorageS3(httpRootDir, compilerProps, awsProps); mockDynamoDb.on(QueryCommand, {TableName: 'table'}).resolves({ Items: [ { full_hash: {S: 'ABCDEFGHIZZ'}, unique_subhash: {S: 'ABCDEFGHI'}, }, ], }); await expect(storage.findUniqueSubhash('ABCDEFGHIJKLMNOPQRSTUV')).resolves.toEqual({ alreadyPresent: false, prefix: 'ABCDEF', uniqueSubHash: 'ABCDEFGHIJ', }); }); it('finds an existing match', async () => { const storage = new StorageS3(httpRootDir, compilerProps, awsProps); mockDynamoDb.on(QueryCommand, {TableName: 'table'}).resolves({ Items: [ { full_hash: {S: 'ABCDEFGHIJKLMNOPQRSTUV'}, unique_subhash: {S: 'ABCDEFGHI'}, }, ], }); await expect(storage.findUniqueSubhash('ABCDEFGHIJKLMNOPQRSTUV')).resolves.toEqual({ alreadyPresent: true, prefix: 'ABCDEF', uniqueSubHash: 'ABCDEFGHI', }); }); }); describe('Stores to s3', () => { const mockDynamoDb = mockClient(DynamoDB); const mockS3 = mockClient(S3); beforeEach(() => { mockDynamoDb.reset(); mockS3.reset(); }); const httpRootDir = '/'; const compilerProps = properties.fakeProps({}); const awsProps = properties.fakeProps({ region: 'not-a-region', storageBucket: 'bucket', storagePrefix: 'prefix', storageDynamoTable: 'table', }); it('and works ok', async () => { const storage = new StorageS3(httpRootDir, compilerProps, awsProps); const object = { prefix: 'ABCDEF', uniqueSubHash: 'ABCDEFG', fullHash: 'ABCDEFGHIJKLMNOP', config: 'yo', }; await storage.storeItem(object, {get: () => 'localhost'}); expect( mockS3.commandCalls(PutObjectCommand, { Bucket: 'bucket', Key: 'prefix/ABCDEFGHIJKLMNOP', Body: 'yo', }), ).toHaveLength(1); expect( mockDynamoDb.commandCalls(PutItemCommand, { TableName: 'table', Item: { prefix: {S: 'ABCDEF'}, unique_subhash: {S: 'ABCDEFG'}, full_hash: {S: 'ABCDEFGHIJKLMNOP'}, stats: {M: {clicks: {N: '0'}}}, creation_ip: {S: 'localhost'}, }, }), ).toHaveLength(1); }); }); describe('Retrieves from s3', () => { const mockDynamoDb = mockClient(DynamoDB); const mockS3 = mockClient(S3); beforeEach(() => { mockDynamoDb.reset(); mockS3.reset(); }); const httpRootDir = '/'; const compilerProps = properties.fakeProps({}); const awsProps = properties.fakeProps({ region: 'not-a-region', storageBucket: 'bucket', storagePrefix: 'prefix', storageDynamoTable: 'table', }); it('fetches in the happy path', async () => { const storage = new StorageS3(httpRootDir, compilerProps, awsProps); mockDynamoDb .on(GetItemCommand, { TableName: 'table', Key: { prefix: {S: 'ABCDEF'}, unique_subhash: {S: 'ABCDEF'}, }, }) .resolves({Item: {full_hash: {S: 'ABCDEFGHIJKLMNOP'}}}); const stream = new Readable(); stream.push('I am a monkey'); stream.push(null); mockS3 .on(GetObjectCommand, { Bucket: 'bucket', Key: 'prefix/ABCDEFGHIJKLMNOP', }) .resolves({Body: sdkStreamMixin(stream)}); const result = await storage.expandId('ABCDEF'); expect(result).toEqual({config: 'I am a monkey'}); }); it('should handle failures', async () => { const storage = new StorageS3(httpRootDir, compilerProps, awsProps); mockDynamoDb.on(GetItemCommand).resolves({}); await expect(storage.expandId('ABCDEF')).rejects.toThrow('ID ABCDEF not present in links table'); }); }); describe('Updates counts in s3', async () => { const mockDynamoDb = mockClient(DynamoDB); const mockS3 = mockClient(S3); beforeEach(() => { mockDynamoDb.reset(); mockS3.reset(); }); const httpRootDir = '/'; const compilerProps = properties.fakeProps({}); const awsProps = properties.fakeProps({ region: 'not-a-region', storageBucket: 'bucket', storagePrefix: 'prefix', storageDynamoTable: 'table', }); it('should increment for simple cases', async () => { const storage = new StorageS3(httpRootDir, compilerProps, awsProps); await storage.incrementViewCount('ABCDEF'); expect( mockDynamoDb.commandCalls(UpdateItemCommand, { ExpressionAttributeValues: {':inc': {N: '1'}}, Key: { prefix: {S: 'ABCDEF'}, unique_subhash: {S: 'ABCDEF'}, }, ReturnValues: 'NONE', TableName: 'table', UpdateExpression: 'SET stats.clicks = stats.clicks + :inc', }), ).toHaveLength(1); }); });