-
Notifications
You must be signed in to change notification settings - Fork 479
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
Add a test to check the download of the latest version #648
base: master
Are you sure you want to change the base?
Changes from all commits
78997dd
5231691
5380321
f920076
8b4c2e5
e054e70
515ec2b
ad40b4c
5893e2e
26a0167
461fa71
6e7f694
1b16bb9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import * as fs from 'fs'; | ||
import { https } from 'follow-redirects'; | ||
import MemoryStream from 'memorystream'; | ||
import { hashFile } from './common/helpers'; | ||
|
||
function getVersionList (host: string): Promise<string> { | ||
console.log('Retrieving available version list...'); | ||
|
||
return new Promise<string>((resolve, reject) => { | ||
const mem = new MemoryStream(null, { readable: false }); | ||
https.get(`${host}/bin/list.json`, function (response) { | ||
if (response.statusCode !== 200) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's something that has already been here before but we should change this so that anything between 200 and 299 is a success. I think we also have the same check in other places in the code But I'd do it in a separate PR since it's a functional change, not just a refactor and is also unrelated to tests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, you're right, I was actually looking at the fetch API when I made the comment. |
||
reject(new Error('Error downloading file: ' + response.statusCode)); | ||
} | ||
response.pipe(mem); | ||
response.on('end', function () { | ||
resolve(mem.toString()); | ||
}); | ||
}).on('error', (err) => { | ||
reject(err); | ||
}); | ||
}); | ||
} | ||
|
||
function downloadBinary (host: string, outputName: string, releaseFile: string, expectedHash: string): Promise<void> { | ||
console.log('Downloading version', releaseFile); | ||
|
||
return new Promise<void>((resolve, reject) => { | ||
// Remove if existing | ||
if (fs.existsSync(outputName)) { | ||
fs.unlinkSync(outputName); | ||
} | ||
|
||
process.on('SIGINT', function () { | ||
fs.unlinkSync(outputName); | ||
reject(new Error('Interrupted... file removed')); | ||
}); | ||
|
||
const file = fs.createWriteStream(outputName, { encoding: 'binary' }); | ||
https.get(`${host}/bin/${releaseFile}`, function (response) { | ||
if (response.statusCode !== 200) { | ||
reject(new Error('Error downloading file: ' + response.statusCode)); | ||
} | ||
response.pipe(file); | ||
file.on('finish', function () { | ||
file.close(); | ||
const hash = hashFile(outputName); | ||
if (expectedHash !== hash) { | ||
reject(new Error('Hash mismatch: expected ' + expectedHash + ' but got ' + hash)); | ||
} else { | ||
console.log('Done.'); | ||
resolve(); | ||
} | ||
}); | ||
}).on('error', (err) => { | ||
reject(err); | ||
}); | ||
}); | ||
} | ||
|
||
export = { | ||
getVersionList, | ||
downloadBinary | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
import * as tmp from 'tmp'; | ||
import tape from 'tape'; | ||
import nock from 'nock'; | ||
import fs from 'fs'; | ||
import path from 'path'; | ||
import { https } from 'follow-redirects'; | ||
import downloader from '../downloader'; | ||
import { keccak256 } from 'js-sha3'; | ||
import { hashFile } from '../common/helpers'; | ||
|
||
const assets = path.resolve(__dirname, 'resources/assets'); | ||
|
||
tape.onFinish(() => { | ||
if (!nock.isDone()) { | ||
throw Error('Expected download requests were not performed'); | ||
} | ||
}); | ||
|
||
function generateTestFile (t: tape.Test, content: string): tmp.FileResult { | ||
// As the `keep` option is set to true the removeCallback must be called by the caller | ||
// to cleanup the files after the test. | ||
const file = tmp.fileSync({ template: 'soljson-XXXXXX.js', keep: true }); | ||
try { | ||
fs.writeFileSync(file.name, content); | ||
} catch (err) { | ||
t.fail(`Error writing test file: ${err.message}`); | ||
} | ||
|
||
return file; | ||
} | ||
|
||
function versionListMock (host: string): nock.Interceptor { | ||
return nock(host).get('/bin/list.json'); | ||
} | ||
|
||
function downloadBinaryMock (host: string, filename: string): nock.Interceptor { | ||
return nock(host).get(`/bin/${path.basename(filename)}`); | ||
} | ||
|
||
function defaultListener (req: any, res: any): void { | ||
res.writeHead(200); | ||
res.end('OK'); | ||
}; | ||
|
||
async function startMockServer (listener = defaultListener): Promise<any> { | ||
const server = https.createServer({ | ||
key: fs.readFileSync(path.resolve(assets, 'key.pem')), | ||
cert: fs.readFileSync(path.resolve(assets, 'cert.pem')) | ||
}, listener); | ||
|
||
await new Promise(resolve => server.listen(resolve)); | ||
server.port = server.address().port; | ||
server.origin = `https://localhost:${server.port}`; | ||
return server; | ||
} | ||
|
||
tape('Download version list', async function (t) { | ||
const server = await startMockServer(); | ||
|
||
t.teardown(function () { | ||
server.close(); | ||
nock.cleanAll(); | ||
}); | ||
|
||
t.test('successfully get version list', async function (st) { | ||
const dummyListPath = path.resolve(assets, 'dummy-list.json'); | ||
versionListMock(server.origin).replyWithFile(200, dummyListPath, { | ||
'Content-Type': 'application/json' | ||
}); | ||
|
||
try { | ||
const list = JSON.parse( | ||
await downloader.getVersionList(server.origin) | ||
); | ||
const expected = require(dummyListPath); | ||
st.deepEqual(list, expected, 'list should match'); | ||
st.equal(list.latestRelease, expected.latestRelease, 'latest release should be equal'); | ||
} catch (err) { | ||
st.fail(err.message); | ||
} | ||
st.end(); | ||
}); | ||
|
||
t.test('should throw an exception when version list not found', async function (st) { | ||
versionListMock(server.origin).reply(404); | ||
|
||
try { | ||
await downloader.getVersionList(server.origin); | ||
st.fail('should throw file not found error'); | ||
} catch (err) { | ||
st.equal(err.message, 'Error downloading file: 404', 'should throw file not found error'); | ||
} | ||
st.end(); | ||
}); | ||
}); | ||
|
||
tape('Download binary', async function (t) { | ||
const server = await startMockServer(); | ||
const content = '() => {}'; | ||
const tmpDir = tmp.dirSync({ unsafeCleanup: true, prefix: 'solcjs-download-test-' }).name; | ||
|
||
t.teardown(function () { | ||
server.close(); | ||
nock.cleanAll(); | ||
}); | ||
|
||
t.test('successfully download binary', async function (st) { | ||
const targetFilename = `${tmpDir}/target-success.js`; | ||
const file = generateTestFile(st, content); | ||
|
||
st.teardown(function () { | ||
file.removeCallback(); | ||
}); | ||
|
||
downloadBinaryMock(server.origin, file.name) | ||
.replyWithFile(200, file.name, { | ||
'content-type': 'application/javascript', | ||
'content-length': content.length.toString() | ||
}); | ||
|
||
try { | ||
await downloader.downloadBinary( | ||
server.origin, | ||
targetFilename, | ||
file.name, | ||
hashFile(file.name) | ||
); | ||
|
||
if (!fs.existsSync(targetFilename)) { | ||
st.fail('download failed'); | ||
} | ||
|
||
const got = fs.readFileSync(targetFilename, { encoding: 'binary' }); | ||
const expected = fs.readFileSync(file.name, { encoding: 'binary' }); | ||
st.equal(got.length, expected.length, 'should download the correct file'); | ||
} catch (err) { | ||
st.fail(err.message); | ||
} | ||
st.end(); | ||
}); | ||
|
||
t.test('should throw an exception when file not found', async function (st) { | ||
const targetFilename = `${tmpDir}/target-fail404.js`; | ||
downloadBinaryMock(server.origin, 'test.js').reply(404); | ||
|
||
try { | ||
await downloader.downloadBinary( | ||
server.origin, | ||
targetFilename, | ||
'test.js', | ||
`0x${keccak256('something')}` | ||
); | ||
st.fail('should throw file not found error'); | ||
} catch (err) { | ||
st.equal(err.message, 'Error downloading file: 404', 'should throw file not found error'); | ||
} | ||
st.end(); | ||
}); | ||
|
||
t.test('should throw an exception if hashes do not match', async function (st) { | ||
const targetFilename = `${tmpDir}/target-fail-hash.js`; | ||
const file = generateTestFile(st, content); | ||
|
||
st.teardown(function () { | ||
file.removeCallback(); | ||
}); | ||
|
||
downloadBinaryMock(server.origin, file.name) | ||
.replyWithFile(200, file.name, { | ||
'content-type': 'application/javascript', | ||
'content-length': content.length.toString() | ||
}); | ||
|
||
try { | ||
await downloader.downloadBinary( | ||
server.origin, | ||
targetFilename, | ||
file.name, | ||
`0x${keccak256('something')}` | ||
); | ||
st.fail('should throw hash mismatch error'); | ||
} catch (err) { | ||
st.match(err.message, /Hash mismatch/, 'should detect hash mismatch'); | ||
} | ||
st.end(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
The certificates in this folder are only for testing purposes and are **not** valid certificates. | ||
|
||
They were generated using the following command: | ||
- Generate an RSA private key: | ||
`openssl genrsa -out key.pem` | ||
- Generate a csr using all default options and common name as "localhost": | ||
`openssl req -new -key key.pem -out csr.pem` | ||
- Self-sign the certificate: | ||
`openssl x509 -req -days 9999 -in csr.pem -signkey key.pem -out cert.pem` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason to directly use promises rather than
async
/await
here?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. As far as I know the
https.get
module from nodejs is not "awaitable" so I needed to wrap it in a promise and explicitly resolve the response.