Skip to content

Commit 59a65e0

Browse files
committed
add tests for fallback
1 parent d53c120 commit 59a65e0

File tree

4 files changed

+169
-5
lines changed

4 files changed

+169
-5
lines changed

lib/install.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ function place_binary_authenticated(opts, targetDir, callback) {
2929
log.info('install', 'Attempting authenticated S3 download');
3030

3131
// Check if AWS credentials are available
32-
if (!process.env.AWS_ACCESS_KEY_ID && !process.env.AWS_SECRET_ACCESS_KEY) {
32+
if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) {
3333
const err = new Error('Binary is private but AWS credentials not found. Please configure AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables, or use --fallback-to-build to compile from source.');
3434
err.statusCode = 403;
3535
return callback(err);
@@ -127,7 +127,10 @@ function place_binary(uri, targetDir, opts, callback) {
127127
// If we get 403 Forbidden, the binary might be private - try authenticated download
128128
if (res.status === 403) {
129129
log.info('install', 'Received 403 Forbidden - attempting authenticated download');
130-
return place_binary_authenticated(opts, targetDir, callback);
130+
// Call place_binary_authenticated and return a special marker
131+
// to prevent the promise chain from calling callback again
132+
place_binary_authenticated(opts, targetDir, callback);
133+
return { authenticated: true };
131134
}
132135
throw new Error(`response status ${res.status} ${res.statusText} on ${sanitized}`);
133136
}
@@ -153,6 +156,9 @@ function place_binary(uri, targetDir, opts, callback) {
153156
});
154157
})
155158
.then((text) => {
159+
if (text && text.authenticated) {
160+
return; // Don't call callback - place_binary_authenticated will handle it
161+
}
156162
log.info(text);
157163
callback();
158164
})

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@mapbox/node-pre-gyp",
33
"description": "Node.js native addon binary install tool",
4-
"version": "2.0.2",
4+
"version": "2.0.2-beta",
55
"keywords": [
66
"native",
77
"addon",

test/private-binary.test.js

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
'use strict';
2+
3+
const fs = require('fs');
4+
const test = require('tape');
5+
const nock = require('nock');
6+
const install = require('../lib/install.js');
7+
8+
// Dummy tar.gz data - contains a blank directory
9+
const targz = 'H4sICPr8u1oCA3gudGFyANPTZ6A5MDAwMDc1VQDTZhAaCGA0hGNobGRqZm5uZmxupGBgaGhiZsKgYMpAB1BaXJJYBHRKYk5pcioedeUZqak5+D2J5CkFhlEwCkbBKBjkAAAyG1ofAAYAAA==';
10+
11+
test('should fallback to authenticated download on 403 Forbidden', (t) => {
12+
process.env.node_pre_gyp_mock_s3 = 'true';
13+
process.env.AWS_ACCESS_KEY_ID = 'mock-key';
14+
process.env.AWS_SECRET_ACCESS_KEY = 'mock-secret';
15+
16+
const origin = 'https://npg-mock-bucket.s3.us-east-1.amazonaws.com';
17+
nock.cleanAll();
18+
19+
// Mock the public HTTPS request to return 403
20+
const publicScope = nock(origin)
21+
.get(/\/node-pre-gyp\/node-pre-gyp-test-app1\/v0.1.0\/Release\/node-v\d+-\S+.tar.gz/)
22+
.reply(403, 'Forbidden');
23+
24+
const opts = {
25+
opts: {
26+
'build-from-source': false,
27+
'update-binary': true
28+
}
29+
};
30+
31+
process.chdir('test/app1');
32+
opts.package_json = JSON.parse(fs.readFileSync('./package.json'));
33+
opts.package_json.binary.host = origin;
34+
35+
install(opts, [], (err) => {
36+
delete process.env.node_pre_gyp_mock_s3;
37+
delete process.env.AWS_ACCESS_KEY_ID;
38+
delete process.env.AWS_SECRET_ACCESS_KEY;
39+
40+
t.ok(publicScope.isDone(), 'Public HTTPS request was attempted');
41+
if (err) {
42+
t.ok(err.message.includes('does not exist') || err.message.includes('NotFound'),
43+
'Error should be about missing file in S3, not credentials');
44+
} else {
45+
t.pass('Authenticated download succeeded');
46+
}
47+
48+
nock.cleanAll();
49+
t.end();
50+
});
51+
});
52+
53+
test('should fail gracefully when 403 and no AWS credentials', (t) => {
54+
try {
55+
process.chdir('test/app1');
56+
} catch (e) {
57+
// Already in test/app1 from previous test
58+
}
59+
60+
// Ensure no AWS credentials
61+
delete process.env.AWS_ACCESS_KEY_ID;
62+
delete process.env.AWS_SECRET_ACCESS_KEY;
63+
delete process.env.node_pre_gyp_mock_s3;
64+
65+
const origin = 'https://npg-mock-bucket.s3.us-east-1.amazonaws.com';
66+
67+
nock.cleanAll();
68+
const publicScope = nock(origin)
69+
.get(/\/node-pre-gyp\/node-pre-gyp-test-app1\/v0.1.0\/Release\/node-v\d+-\S+.tar.gz/)
70+
.reply(403, 'Forbidden');
71+
72+
const opts = {
73+
opts: {
74+
'build-from-source': false,
75+
'update-binary': true
76+
}
77+
};
78+
79+
opts.package_json = JSON.parse(fs.readFileSync('./package.json'));
80+
opts.package_json.binary.host = origin;
81+
82+
install(opts, [], (err) => {
83+
t.ok(err, 'Should error without credentials');
84+
t.ok(err.message.includes('AWS credentials not found'), 'Error should mention missing credentials');
85+
t.equal(err.statusCode, 403, 'Error should have 403 status code');
86+
t.ok(publicScope.isDone(), 'Public HTTPS request was attempted');
87+
88+
nock.cleanAll();
89+
t.end();
90+
});
91+
});
92+
93+
test('should succeed with public binary (no 403)', (t) => {
94+
try {
95+
process.chdir('test/app1');
96+
} catch (e) {
97+
// Already in test/app1
98+
}
99+
100+
const origin = 'https://npg-mock-bucket.s3.us-east-1.amazonaws.com';
101+
nock.cleanAll();
102+
const scope = nock(origin)
103+
.get(/\/node-pre-gyp\/node-pre-gyp-test-app1\/v0.1.0\/Release\/node-v\d+-\S+.tar.gz/)
104+
.reply(200, Buffer.from(targz, 'base64'));
105+
106+
const opts = {
107+
opts: {
108+
'build-from-source': false,
109+
'update-binary': true
110+
}
111+
};
112+
113+
opts.package_json = JSON.parse(fs.readFileSync('./package.json'));
114+
opts.package_json.binary.host = origin;
115+
116+
install(opts, [], (err) => {
117+
t.ifError(err, 'Public binary install should succeed');
118+
t.ok(scope.isDone(), 'Public download was completed');
119+
120+
nock.cleanAll();
121+
t.end();
122+
});
123+
});
124+
125+
test('should handle 404 without triggering authenticated fallback', (t) => {
126+
try {
127+
process.chdir('test/app1');
128+
} catch (e) {
129+
// Already in test/app1
130+
}
131+
132+
const origin = 'https://npg-mock-bucket.s3.us-east-1.amazonaws.com';
133+
134+
nock.cleanAll();
135+
const scope = nock(origin)
136+
.get(/\/node-pre-gyp\/node-pre-gyp-test-app1\/v0.1.0\/Release\/node-v\d+-\S+.tar.gz/)
137+
.reply(404, 'Not Found');
138+
139+
const opts = {
140+
opts: {
141+
'build-from-source': false,
142+
'update-binary': true
143+
}
144+
};
145+
146+
opts.package_json = JSON.parse(fs.readFileSync('./package.json'));
147+
opts.package_json.binary.host = origin;
148+
149+
install(opts, [], (err) => {
150+
t.ok(err, 'Should error with 404');
151+
t.ok(err.message.includes('404'), 'Error message should mention 404');
152+
t.notOk(err.message.includes('credentials'), 'Should not mention credentials for 404');
153+
t.ok(scope.isDone(), 'HTTP request was completed');
154+
155+
nock.cleanAll();
156+
t.end();
157+
});
158+
});

0 commit comments

Comments
 (0)