helpers = require('../helpers') AWS = helpers.AWS Stream = require('stream').Stream Buffer = AWS.util.Buffer describe 'AWS.S3', -> s3 = null request = (operation, params) -> s3.makeRequest(operation, params) beforeEach -> s3 = new AWS.S3(region: undefined) describe 'dnsCompatibleBucketName', -> it 'must be at least 3 characters', -> expect(s3.dnsCompatibleBucketName('aa')).toBe(false) it 'must not be longer than 63 characters', -> b = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' expect(s3.dnsCompatibleBucketName(b)).toBe(false) it 'must start with a lower-cased letter or number', -> expect(s3.dnsCompatibleBucketName('Abc')).toBe(false) expect(s3.dnsCompatibleBucketName('-bc')).toBe(false) expect(s3.dnsCompatibleBucketName('abc')).toBe(true) it 'must end with a lower-cased letter or number', -> expect(s3.dnsCompatibleBucketName('abC')).toBe(false) expect(s3.dnsCompatibleBucketName('ab-')).toBe(false) expect(s3.dnsCompatibleBucketName('abc')).toBe(true) it 'may not contain multiple contiguous dots', -> expect(s3.dnsCompatibleBucketName('abc.123')).toBe(true) expect(s3.dnsCompatibleBucketName('abc..123')).toBe(false) it 'may only contain letters numbers and dots', -> expect(s3.dnsCompatibleBucketName('abc123')).toBe(true) expect(s3.dnsCompatibleBucketName('abc_123')).toBe(false) it 'must not look like an ip address', -> expect(s3.dnsCompatibleBucketName('1.2.3.4')).toBe(false) expect(s3.dnsCompatibleBucketName('a.b.c.d')).toBe(true) describe 'endpoint', -> it 'sets hostname to s3.amazonaws.com when region is un-specified', -> s3 = new AWS.S3(region: undefined) expect(s3.endpoint.hostname).toEqual('s3.amazonaws.com') it 'sets hostname to s3.amazonaws.com when region is us-east-1', -> s3 = new AWS.S3(region: 'us-east-1') expect(s3.endpoint.hostname).toEqual('s3.amazonaws.com') it 'sets region to us-east-1 when unspecified', -> s3 = new AWS.S3(region: 'us-east-1') expect(s3.config.region).toEqual('us-east-1') it 'combines the region with s3 in the endpoint using a - instead of .', -> s3 = new AWS.S3(region: 'us-west-1') expect(s3.endpoint.hostname).toEqual('s3-us-west-1.amazonaws.com') describe 'building a request', -> build = (operation, params) -> request(operation, params).build().httpRequest it 'obeys the configuration for s3ForcePathStyle', -> config = new AWS.Config(s3ForcePathStyle: true, accessKeyId: 'AKID', secretAccessKey: 'SECRET') s3 = new AWS.S3(config) expect(s3.config.s3ForcePathStyle).toEqual(true) req = build('headObject', {Bucket:'bucket', Key:'key'}) expect(req.endpoint.hostname).toEqual('s3.amazonaws.com') expect(req.path).toEqual('/bucket/key') describe 'uri escaped params', -> it 'uri-escapes path and querystring params', -> # bucket param ends up as part of the hostname params = { Bucket: 'bucket', Key: 'a b c', VersionId: 'a&b' } req = build('headObject', params) expect(req.path).toEqual('/a%20b%20c?versionId=a%26b') it 'does not uri-escape forward slashes in the path', -> params = { Bucket: 'bucket', Key: 'k e/y' } req = build('headObject', params) expect(req.path).toEqual('/k%20e/y') it 'ensures a single forward slash exists', -> req = build('listObjects', { Bucket: 'bucket' }) expect(req.path).toEqual('/') req = build('listObjects', { Bucket: 'bucket', MaxKeys:123 }) expect(req.path).toEqual('/?max-keys=123') describe 'adding Content-Type', -> beforeEach -> spyOn(AWS.util, 'isBrowser').andReturn(true) it 'adds default content-type when not supplied', -> req = build('putObject', Bucket: 'bucket', Key: 'key', Body: 'body') expect(req.headers['Content-Type']).toEqual('application/octet-stream; charset=UTF-8') it 'adds charset to existing content-type if not supplied', -> req = build('putObject', Bucket: 'bucket', Key: 'key', Body: 'body', ContentType: 'text/html') expect(req.headers['Content-Type']).toEqual('text/html; charset=UTF-8') it 'normalized charset to uppercase', -> req = build('putObject', Bucket: 'bucket', Key: 'key', Body: 'body', ContentType: 'text/html; charset=utf-8') expect(req.headers['Content-Type']).toEqual('text/html; charset=UTF-8') it 'does not add charset to non-string data', -> req = build('putObject', Bucket: 'bucket', Key: 'key', Body: new Buffer('body'), ContentType: 'image/png') expect(req.headers['Content-Type']).toEqual('image/png') describe 'virtual-hosted vs path-style bucket requests', -> describe 'HTTPS', -> beforeEach -> s3 = new AWS.S3(sslEnabled: true, region: undefined) it 'puts dns-compat bucket names in the hostname', -> req = build('headObject', {Bucket:'bucket-name',Key:'abc'}) expect(req.method).toEqual('HEAD') expect(req.endpoint.hostname).toEqual('bucket-name.s3.amazonaws.com') expect(req.path).toEqual('/abc') it 'ensures the path contains / at a minimum when moving bucket', -> req = build('listObjects', {Bucket:'bucket-name'}) expect(req.endpoint.hostname).toEqual('bucket-name.s3.amazonaws.com') expect(req.path).toEqual('/') it 'puts dns-compat bucket names in path if they contain a dot', -> req = build('listObjects', {Bucket:'bucket.name'}) expect(req.endpoint.hostname).toEqual('s3.amazonaws.com') expect(req.path).toEqual('/bucket.name') it 'puts dns-compat bucket names in path if configured to do so', -> s3 = new AWS.S3(sslEnabled: true, s3ForcePathStyle: true, region: undefined) req = build('listObjects', {Bucket:'bucket-name'}) expect(req.endpoint.hostname).toEqual('s3.amazonaws.com') expect(req.path).toEqual('/bucket-name') it 'puts dns-incompat bucket names in path', -> req = build('listObjects', {Bucket:'bucket_name'}) expect(req.endpoint.hostname).toEqual('s3.amazonaws.com') expect(req.path).toEqual('/bucket_name') describe 'HTTP', -> beforeEach -> s3 = new AWS.S3(sslEnabled: false, region: undefined) it 'puts dns-compat bucket names in the hostname', -> req = build('listObjects', {Bucket:'bucket-name'}) expect(req.endpoint.hostname).toEqual('bucket-name.s3.amazonaws.com') expect(req.path).toEqual('/') it 'puts dns-compat bucket names in the hostname if they contain a dot', -> req = build('listObjects', {Bucket:'bucket.name'}) expect(req.endpoint.hostname).toEqual('bucket.name.s3.amazonaws.com') expect(req.path).toEqual('/') it 'puts dns-incompat bucket names in path', -> req = build('listObjects', {Bucket:'bucket_name'}) expect(req.endpoint.hostname).toEqual('s3.amazonaws.com') expect(req.path).toEqual('/bucket_name') describe 'SSE support', -> beforeEach -> s3 = new AWS.S3 it 'encodes SSECustomerKey and fills in MD5', -> req = s3.putObject Bucket: 'bucket', Key: 'key', Body: 'data' SSECustomerKey: 'KEY', SSECustomerAlgorithm: 'AES256' req.build() expect(req.httpRequest.headers['x-amz-server-side-encryption-customer-key']). toEqual('S0VZ') expect(req.httpRequest.headers['x-amz-server-side-encryption-customer-key-MD5']). toEqual('TzFsSjRNSnJoM1o2UjFLaWR0NlZjQT09') it 'encodes CopySourceSSECustomerKey and fills in MD5', -> req = s3.copyObject Bucket: 'bucket', Key: 'key', CopySource: 'bucket/oldkey', Body: 'data' CopySourceSSECustomerKey: 'KEY', CopySourceSSECustomerAlgorithm: 'AES256' req.build() expect(req.httpRequest.headers['x-amz-copy-source-server-side-encryption-customer-key']). toEqual('S0VZ') expect(req.httpRequest.headers['x-amz-copy-source-server-side-encryption-customer-key-MD5']). toEqual('TzFsSjRNSnJoM1o2UjFLaWR0NlZjQT09') describe 'retry behavior', -> it 'retries RequestTimeout errors', -> s3.config.maxRetries = 3 helpers.mockHttpResponse 400, {}, 'RequestTimeoutmessage' s3.putObject (err, data) -> expect(@retryCount).toEqual(s3.config.maxRetries) # S3 returns a handful of errors without xml bodies (to match the # http spec) these tests ensure we give meaningful codes/messages for these. describe 'errors with no XML body', -> extractError = (statusCode, body) -> req = request('operation') resp = new AWS.Response(req) resp.httpResponse.body = new Buffer(body || '') resp.httpResponse.statusCode = statusCode req.emit('extractError', [resp]) resp.error it 'handles 304 errors', -> error = extractError(304) expect(error.code).toEqual('NotModified') expect(error.message).toEqual(null) it 'handles 400 errors', -> error = extractError(400) expect(error.code).toEqual('BadRequest') expect(error.message).toEqual(null) it 'handles 403 errors', -> error = extractError(403) expect(error.code).toEqual('Forbidden') expect(error.message).toEqual(null) it 'handles 404 errors', -> error = extractError(404) expect(error.code).toEqual('NotFound') expect(error.message).toEqual(null) it 'misc errors not known to return an empty body', -> error = extractError(412) # made up expect(error.code).toEqual(412) expect(error.message).toEqual(null) it 'uses canned errors only when the body is empty', -> body = """ ErrorCode ErrorMessage """ error = extractError(403, body) expect(error.code).toEqual('ErrorCode') expect(error.message).toEqual('ErrorMessage') # tests from this point on are "special cases" for specific aws operations describe 'getBucketAcl', -> it 'correctly parses the ACL XML document', -> headers = { 'x-amz-request-id' : 'request-id' } body = """ aws-sdk id FULL_CONTROL uri READ aws-sdk id """ helpers.mockHttpResponse 200, headers, body s3.getBucketAcl (error, data) -> expect(error).toBe(null) expect(data).toEqual({ Owner: DisplayName: 'aws-sdk', ID: 'id' Grants: [ { Permission: 'FULL_CONTROL' Grantee: Type: 'CanonicalUser' DisplayName: 'aws-sdk' ID: 'id' }, { Permission : 'READ' Grantee: Type: 'Group' URI: 'uri' } ] }) describe 'putBucketAcl', -> it 'correctly builds the ACL XML document', -> xml = """ aws-sdk id FULL_CONTROL uri READ aws-sdk id """ helpers.mockHttpResponse 200, {}, '' params = AccessControlPolicy: Owner: DisplayName: 'aws-sdk', ID: 'id' Grants: [ { Permission: 'FULL_CONTROL' Grantee: Type: 'CanonicalUser', DisplayName: 'aws-sdk' ID: 'id' }, { Permission : 'READ' Grantee: Type: 'Group', URI: 'uri' } ] s3.putBucketAcl params, (err, data) -> helpers.matchXML(this.request.httpRequest.body, xml) describe 'completeMultipartUpload', -> it 'returns data when the resp is 200 with valid response', -> headers = 'x-amz-id-2': 'Uuag1LuByRx9e6j5Onimru9pO4ZVKnJ2Qz7/C1NPcfTWAtRPfTaOFg==' 'x-amz-request-id': '656c76696e6727732072657175657374' body = """ http://Example-Bucket.s3.amazonaws.com/Example-Object Example-Bucket Example-Object "3858f62230ac3c915f300c664312c11f-9" """ helpers.mockHttpResponse 200, headers, body s3.completeMultipartUpload (error, data) -> expect(error).toBe(null) expect(data).toEqual({ Location: 'http://Example-Bucket.s3.amazonaws.com/Example-Object' Bucket: 'Example-Bucket' Key: 'Example-Object' ETag: '"3858f62230ac3c915f300c664312c11f-9"' }) expect(this.requestId).toEqual('656c76696e6727732072657175657374') it 'returns an error when the resp is 200 with an error xml document', -> body = """ InternalError We encountered an internal error. Please try again. 656c76696e6727732072657175657374 Uuag1LuByRx9e6j5Onimru9pO4ZVKnJ2Qz7/C1NPcfTWAtRPfTaOFg== """ helpers.mockHttpResponse 200, {}, body s3.completeMultipartUpload (error, data) -> expect(error instanceof Error).toBeTruthy() expect(error.code).toEqual('InternalError') expect(error.message).toEqual('We encountered an internal error. Please try again.') expect(error.statusCode).toEqual(200) expect(error.retryable).toEqual(true) expect(data).toEqual(null) describe 'getBucketLocation', -> it 'returns null for the location constraint when not present', -> body = '\n' helpers.mockHttpResponse 200, {}, body s3.getBucketLocation (error, data) -> expect(error).toBe(null) expect(data).toEqual({}) it 'parses the location constraint from the root xml', -> headers = { 'x-amz-request-id': 'abcxyz' } body = '\nEU' helpers.mockHttpResponse 200, headers, body s3.getBucketLocation (error, data) -> expect(error).toBe(null) expect(data).toEqual(LocationConstraint: 'EU') expect(this.requestId).toEqual('abcxyz') describe 'createBucket', -> it 'auto-populates the LocationConstraint based on the region', -> loc = null s3 = new AWS.S3(region:'eu-west-1') s3.makeRequest = (op, params) -> loc = params.CreateBucketConfiguration.LocationConstraint s3.createBucket(Bucket:'name') expect(loc).toEqual('eu-west-1') it 'correctly builds the xml', -> AWS.util.each AWS.S3.prototype.computableChecksumOperations, (operation) -> describe operation, -> it 'forces Content-MD5 header parameter', -> req = s3[operation](Bucket: 'bucket', ContentMD5: '000').build() hash = AWS.util.crypto.md5(req.httpRequest.body, 'base64') expect(req.httpRequest.headers['Content-MD5']).toEqual(hash) describe 'willComputeChecksums', -> willCompute = (operation, opts) -> compute = opts.computeChecksums s3 = new AWS.S3(computeChecksums: compute) req = s3.makeRequest(operation, Bucket: 'example', ContentMD5: opts.hash).build() checksum = req.httpRequest.headers['Content-MD5'] if opts.hash != undefined expect(checksum).toEqual(opts.hash) else realChecksum = AWS.util.crypto.md5(req.httpRequest.body, 'base64') expect(checksum).toEqual(realChecksum) it 'computes checksums if the operation requires it', -> willCompute 'deleteObjects', computeChecksums: true willCompute 'putBucketCors', computeChecksums: true willCompute 'putBucketLifecycle', computeChecksums: true willCompute 'putBucketTagging', computeChecksums: true it 'computes checksums if computeChecksums is off and operation requires it', -> willCompute 'deleteObjects', computeChecksums: false willCompute 'putBucketCors', computeChecksums: false willCompute 'putBucketLifecycle', computeChecksums: false willCompute 'putBucketTagging', computeChecksums: false it 'does not compute checksums if computeChecksums is off', -> willCompute 'putObject', computeChecksums: false, hash: null it 'does not compute checksums if computeChecksums is on and ContentMD5 is provided', -> willCompute 'putBucketAcl', computeChecksums: true, hash: '000' it 'does not compute checksums for Stream objects', -> s3 = new AWS.S3(computeChecksums: true) req = s3.putObject(Bucket: 'example', Key: 'foo', Body: new Stream) expect(req.build(->).httpRequest.headers['Content-MD5']).toEqual(undefined) it 'computes checksums if computeChecksums is on and ContentMD5 is not provided',-> willCompute 'putBucketAcl', computeChecksums: true describe 'getSignedUrl', -> date = null beforeEach -> date = AWS.util.date.getDate AWS.util.date.getDate = -> new Date(0) afterEach -> AWS.util.date.getDate = date it 'gets a signed URL for getObject', -> url = s3.getSignedUrl('getObject', Bucket: 'bucket', Key: 'key') expect(url).toEqual('https://bucket.s3.amazonaws.com/key?AWSAccessKeyId=akid&Expires=900&Signature=4mlYnRmz%2BBFEPrgYz5tXcl9Wc4w%3D&x-amz-security-token=session') it 'gets a signed URL with Expires time', -> url = s3.getSignedUrl('getObject', Bucket: 'bucket', Key: 'key', Expires: 60) expect(url).toEqual('https://bucket.s3.amazonaws.com/key?AWSAccessKeyId=akid&Expires=60&Signature=kH2pMK%2Fgm7cCZKVG8GHVTRGXKzY%3D&x-amz-security-token=session') it 'gets a signed URL with expiration and bound bucket parameters', -> s3 = new AWS.S3(paramValidation: true, region: undefined, params: Bucket: 'bucket') url = s3.getSignedUrl('getObject', Key: 'key', Expires: 60) expect(url).toEqual('https://bucket.s3.amazonaws.com/key?AWSAccessKeyId=akid&Expires=60&Signature=kH2pMK%2Fgm7cCZKVG8GHVTRGXKzY%3D&x-amz-security-token=session') it 'generates the right URL with a custom endpoint', -> s3 = new AWS.S3(endpoint: 'https://foo.bar.baz:555/prefix', params: Bucket: 'bucket') url = s3.getSignedUrl('getObject', Key: 'key', Expires: 60) expect(url).toEqual('https://bucket.foo.bar.baz:555/prefix/key?AWSAccessKeyId=akid&Expires=60&Signature=zA6k0cQqDkTZgLamfoYLOd%2Bqfg8%3D&x-amz-security-token=session') it 'gets a signed URL with callback', -> url = null runs -> s3.getSignedUrl 'getObject', Bucket: 'bucket', Key: 'key', (err, value) -> url = value waitsFor -> url runs -> expect(url).toEqual('https://bucket.s3.amazonaws.com/key?AWSAccessKeyId=akid&Expires=900&Signature=4mlYnRmz%2BBFEPrgYz5tXcl9Wc4w%3D&x-amz-security-token=session') it 'gets a signed URL for putObject with no body', -> url = s3.getSignedUrl('putObject', Bucket: 'bucket', Key: 'key') expect(url).toEqual('https://bucket.s3.amazonaws.com/key?AWSAccessKeyId=akid&Expires=900&Signature=J%2BnWZ0lPUfLV0kio8ONhJmAttGc%3D&x-amz-security-token=session') it 'gets a signed URL for putObject with special characters', -> url = s3.getSignedUrl('putObject', Bucket: 'bucket', Key: '!@#$%^&*();\':"{}[],./?`~') expect(url).toEqual('https://bucket.s3.amazonaws.com/%21%40%23%24%25%5E%26%2A%28%29%3B%27%3A%22%7B%7D%5B%5D%2C./%3F%60~?AWSAccessKeyId=akid&Expires=900&Signature=9nEltJACZKsriZqU2cmRel6g8LQ%3D&x-amz-security-token=session') it 'gets a signed URL for putObject with a body (and checksum)', -> url = s3.getSignedUrl('putObject', Bucket: 'bucket', Key: 'key', Body: 'body') expect(url).toEqual('https://bucket.s3.amazonaws.com/key?AWSAccessKeyId=akid&Content-MD5=hBotaJrYa9FhFEdFPCLG%2FA%3D%3D&Expires=900&Signature=4ycA2tpHKxfFnNCdqnK1d5BG8gc%3D&x-amz-security-token=session') it 'gets a signed URL and appends to existing query parameters', -> url = s3.getSignedUrl('listObjects', Bucket: 'bucket', Prefix: 'prefix') expect(url).toEqual('https://bucket.s3.amazonaws.com/?AWSAccessKeyId=akid&Expires=900&Signature=8W3pwZPfgucCyPNg1MsoYq8h5zw%3D&prefix=prefix&x-amz-security-token=session') it 'gets a signed URL for getObject using SigV4', -> s3 = new AWS.S3(signatureVersion: 'v4', region: undefined) url = s3.getSignedUrl('getObject', Bucket: 'bucket', Key: 'object') expect(url).toEqual('https://bucket.s3.amazonaws.com/object?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=akid%2F19700101%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=19700101T000000Z&X-Amz-Expires=900&X-Amz-Security-Token=session&X-Amz-Signature=05ae40d2d22c93549a1de0686232ff56baf556876ec497d0d8349431f98b8dfe&X-Amz-SignedHeaders=host') it 'errors when expiry time is greater than a week out on SigV4', -> err = null; data = null s3 = new AWS.S3(signatureVersion: 'v4', region: undefined) params = Bucket: 'bucket', Key: 'object', Expires: 60 * 60 * 24 * 7 + 120 error = 'Presigning does not support expiry time greater than a week with SigV4 signing.' runs -> s3.getSignedUrl 'getObject', params, (e, d) -> data = d; err = e waitsFor -> err || data runs -> expect(err).not.toEqual(null) expect(err.message).toEqual(error) #expect(-> s3.getSignedUrl('getObject', params)).toThrow(error) # sync mode