-
Notifications
You must be signed in to change notification settings - Fork 656
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
fix file upload - consider metadata options by creating form data #500
Changes from 2 commits
e58136b
5feaa6b
e6580bb
70b2d74
99db715
93d8350
73dea90
8e4d4ee
9d0801c
805dd6f
5ce84bb
dee4b03
7f8e5b7
8e4da08
945439a
501b0af
6902c4e
8ce3b61
a206c3e
e96b059
ba661a7
45c7702
c1d65b5
bb58541
e7bbd38
f5596ba
b0a7d94
1899050
6de72b3
7c89cac
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 |
---|---|---|
@@ -1,6 +1,7 @@ | ||
{ | ||
"typescript.tsdk": "./node_modules/typescript/lib", | ||
"editor.rulers": [ | ||
120 | ||
] | ||
} | ||
"typescript.tsdk": "./node_modules/typescript/lib", | ||
"editor.rulers": [ | ||
120 | ||
], | ||
"editor.tabSize": 2, | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -64,12 +64,12 @@ describe('WebClient', function () { | |||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
describe('apiCall()', function() { | ||||||||||||||||||||||||||||||
describe('apiCall()', function () { | ||||||||||||||||||||||||||||||
beforeEach(function () { | ||||||||||||||||||||||||||||||
this.client = new WebClient(token, { retryConfig: fastRetriesForTest }); | ||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
describe('when making a successful call', function() { | ||||||||||||||||||||||||||||||
describe('when making a successful call', function () { | ||||||||||||||||||||||||||||||
beforeEach(function () { | ||||||||||||||||||||||||||||||
this.scope = nock('https://slack.com') | ||||||||||||||||||||||||||||||
.post(/api/) | ||||||||||||||||||||||||||||||
|
@@ -149,9 +149,9 @@ describe('WebClient', function () { | |||||||||||||||||||||||||||||
hum: false, | ||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||
.then(() => { | ||||||||||||||||||||||||||||||
scope.done(); | ||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
.then(() => { | ||||||||||||||||||||||||||||||
scope.done(); | ||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
it.skip('should remove undefined or null values from API arguments'); | ||||||||||||||||||||||||||||||
|
@@ -201,18 +201,28 @@ describe('WebClient', function () { | |||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// intentially vague about the method name | ||||||||||||||||||||||||||||||
return this.client.apiCall('upload', { | ||||||||||||||||||||||||||||||
file: imageBuffer, | ||||||||||||||||||||||||||||||
filename: 'train.png', | ||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||
file: imageBuffer, | ||||||||||||||||||||||||||||||
filename: 'train.png', | ||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||
.then((parts) => { | ||||||||||||||||||||||||||||||
assert.lengthOf(parts.files, 1); | ||||||||||||||||||||||||||||||
const file = parts.files[0]; | ||||||||||||||||||||||||||||||
// options were not provided to the form builder | ||||||||||||||||||||||||||||||
assert.include(file, { fieldname: 'file' }); | ||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
it('should properly serialize when the file is an object with value as a Buffer and options containing filename', function () { | ||||||||||||||||||||||||||||||
const imageBuffer = fs.readFileSync(path.resolve('test', 'fixtures', 'train.jpg')); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// intentially vague about the method name | ||||||||||||||||||||||||||||||
return this.client.apiCall('upload', { | ||||||||||||||||||||||||||||||
file: { value: imageBuffer, options: { filename: 'train.png' } }, | ||||||||||||||||||||||||||||||
filename: 'train.png', | ||||||||||||||||||||||||||||||
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 was trying to avoid this usage all together, because it seems redundant to have to specify the filename twice. can you see any advantage to doing it this way? 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. The main advantage I see is the current implementation state of all the folks who used the old style. I don't know whether there is a need for someone to provide further attributes in the options field (e.g. contentType or knownLength) 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 also want to make sure the implementation accounts for those who used the old style, but i prefer that the path we take to get there is not to redo the same hack, but to smooth out the API so that you don't need to redundantly specify the filename. i wouldn't call it a hack if there was legitimately a use case or need for using the maybe we can channel this effort towards investigating why the following code didn't behave as we want: node-slack-sdk/src/WebClient.ts Lines 530 to 537 in a206c3e
AFAICT it should result in the same thing... also, have you tried this implementation to upload actual files? i'm not 100% confident that the nock expectations are correct. 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. Sorry, but I don't really understand your comment... this code snippet, you mentioned above works now as well. Off course I have tried it before I've opened this PR. And the problem with it, why it didn't work before was in the implementation of node-slack-sdk/src/WebClient.ts Lines 556 to 561 in a206c3e
I've added this part in order to provide the attribute
This is actually the main goal of this PR and all other things we talked here about, were done with intention not to break the current state. As I mentioned in the PR description, you can upload files now with no need of additional/redundant information
Should i remove the 'backup' solution to restrict the file upload to only one way? 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. @KharitonOff thanks so much for your clarification. i missed that diff amidst the rest of the changes but i now see where the issue was before. yes, i'd prefer removing the backup solution so that we don't unnecessarily expand the API contract. if you can do that, i'm happy to land this PR! |
||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||
.then((parts) => { | ||||||||||||||||||||||||||||||
assert.lengthOf(parts.files, 1); | ||||||||||||||||||||||||||||||
const file = parts.files[0]; | ||||||||||||||||||||||||||||||
// TODO: understand why this assertion is failing. already employed the buffer metadata workaround, should | ||||||||||||||||||||||||||||||
// look into the details about whether that workaround is still required, or why else the `source.on` is not | ||||||||||||||||||||||||||||||
// defined error would occur, or if Slack just doesn't need a filename for the part | ||||||||||||||||||||||||||||||
// assert.include(file, { fieldname: 'file', filename: 'train.jpg' }); | ||||||||||||||||||||||||||||||
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. note to self: should the assertion with filename work now? |
||||||||||||||||||||||||||||||
// NOTE: it seems the file and its filename are emitted as a field in addition to the token, not sure if | ||||||||||||||||||||||||||||||
// this was happening in the old implementation. | ||||||||||||||||||||||||||||||
assert.include(file, { fieldname: 'file' }); | ||||||||||||||||||||||||||||||
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. still not asserting that the |
||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
|
@@ -225,8 +235,8 @@ describe('WebClient', function () { | |||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// intentially vague about the method name | ||||||||||||||||||||||||||||||
return this.client.apiCall('upload', { | ||||||||||||||||||||||||||||||
file: imageBuffer, | ||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||
file: imageBuffer, | ||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||
.then(() => { | ||||||||||||||||||||||||||||||
const output = this.capture.getCapturedText(); | ||||||||||||||||||||||||||||||
assert.isNotEmpty(output); | ||||||||||||||||||||||||||||||
|
@@ -241,8 +251,8 @@ describe('WebClient', function () { | |||||||||||||||||||||||||||||
const imageStream = fs.createReadStream(path.resolve('test', 'fixtures', 'train.jpg')); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
return this.client.apiCall('upload', { | ||||||||||||||||||||||||||||||
file: imageStream, | ||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||
file: imageStream, | ||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||
.then((parts) => { | ||||||||||||||||||||||||||||||
assert.lengthOf(parts.files, 1); | ||||||||||||||||||||||||||||||
const file = parts.files[0]; | ||||||||||||||||||||||||||||||
|
@@ -265,20 +275,20 @@ describe('WebClient', function () { | |||||||||||||||||||||||||||||
describe('metadata in the user agent', function () { | ||||||||||||||||||||||||||||||
it('should set the user agent to contain package metadata', function () { | ||||||||||||||||||||||||||||||
const scope = nock('https://slack.com', { | ||||||||||||||||||||||||||||||
reqheaders: { | ||||||||||||||||||||||||||||||
'User-Agent': (value) => { | ||||||||||||||||||||||||||||||
const metadata = parseUserAgentIntoMetadata(value) | ||||||||||||||||||||||||||||||
// NOTE: this assert isn't that strong and doesn't say anything about the values. at this time, there | ||||||||||||||||||||||||||||||
// isn't a good way to test this without dupicating the logic of the code under test. | ||||||||||||||||||||||||||||||
assert.containsAllKeys(metadata, ['node', '@slack:client']); | ||||||||||||||||||||||||||||||
// NOTE: there's an assumption that if there's any keys besides these left at all, its the platform part | ||||||||||||||||||||||||||||||
delete metadata.node; | ||||||||||||||||||||||||||||||
delete metadata['@slack:client']; | ||||||||||||||||||||||||||||||
assert.isNotEmpty(metadata); | ||||||||||||||||||||||||||||||
return true; | ||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||
reqheaders: { | ||||||||||||||||||||||||||||||
'User-Agent': (value) => { | ||||||||||||||||||||||||||||||
const metadata = parseUserAgentIntoMetadata(value) | ||||||||||||||||||||||||||||||
// NOTE: this assert isn't that strong and doesn't say anything about the values. at this time, there | ||||||||||||||||||||||||||||||
// isn't a good way to test this without dupicating the logic of the code under test. | ||||||||||||||||||||||||||||||
assert.containsAllKeys(metadata, ['node', '@slack:client']); | ||||||||||||||||||||||||||||||
// NOTE: there's an assumption that if there's any keys besides these left at all, its the platform part | ||||||||||||||||||||||||||||||
delete metadata.node; | ||||||||||||||||||||||||||||||
delete metadata['@slack:client']; | ||||||||||||||||||||||||||||||
assert.isNotEmpty(metadata); | ||||||||||||||||||||||||||||||
return true; | ||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||
.post(/api/) | ||||||||||||||||||||||||||||||
.reply(200, { ok: true }); | ||||||||||||||||||||||||||||||
return this.client.apiCall('method') | ||||||||||||||||||||||||||||||
|
@@ -291,14 +301,14 @@ describe('WebClient', function () { | |||||||||||||||||||||||||||||
const [name, version] = ['appmedataname', 'appmetadataversion']; | ||||||||||||||||||||||||||||||
addAppMetadata({ name, version }); | ||||||||||||||||||||||||||||||
const scope = nock('https://slack.com', { | ||||||||||||||||||||||||||||||
reqheaders: { | ||||||||||||||||||||||||||||||
'User-Agent': (value) => { | ||||||||||||||||||||||||||||||
const metadata = parseUserAgentIntoMetadata(value) | ||||||||||||||||||||||||||||||
assert.propertyVal(metadata, name, version); | ||||||||||||||||||||||||||||||
return true; | ||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||
reqheaders: { | ||||||||||||||||||||||||||||||
'User-Agent': (value) => { | ||||||||||||||||||||||||||||||
const metadata = parseUserAgentIntoMetadata(value) | ||||||||||||||||||||||||||||||
assert.propertyVal(metadata, name, version); | ||||||||||||||||||||||||||||||
return true; | ||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||
.post(/api/) | ||||||||||||||||||||||||||||||
.reply(200, { ok: true }); | ||||||||||||||||||||||||||||||
// NOTE: appMetaData is only evalued on client construction, so we cannot use the client already created | ||||||||||||||||||||||||||||||
|
@@ -331,7 +341,7 @@ describe('WebClient', function () { | |||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
describe('has option to change slackApiUrl', function() { | ||||||||||||||||||||||||||||||
describe('has option to change slackApiUrl', function () { | ||||||||||||||||||||||||||||||
it('should send requests to an alternative URL', function () { | ||||||||||||||||||||||||||||||
const alternativeUrl = 'http://12.34.56.78/api/'; | ||||||||||||||||||||||||||||||
const scope = nock(alternativeUrl) | ||||||||||||||||||||||||||||||
|
@@ -344,7 +354,7 @@ describe('WebClient', function () { | |||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
describe('has an option to set a custom HTTP agent', function () { | ||||||||||||||||||||||||||||||
// not confident how to test this. one idea is to use sinon to intercept method calls on the agent. | ||||||||||||||||||||||||||||||
it.skip('should send a request using the custom agent', function() { | ||||||||||||||||||||||||||||||
it.skip('should send a request using the custom agent', function () { | ||||||||||||||||||||||||||||||
const agent = new Agent(); | ||||||||||||||||||||||||||||||
const client = new WebClient(token, { agent }); | ||||||||||||||||||||||||||||||
return client.apiCall('method'); | ||||||||||||||||||||||||||||||
|
@@ -390,7 +400,7 @@ describe('WebClient', function () { | |||||||||||||||||||||||||||||
// verify that any requests after maxRequestConcurrency were delayed by the responseDelay | ||||||||||||||||||||||||||||||
const queuedResponses = responses.slice(3); | ||||||||||||||||||||||||||||||
const minDiff = concurrentResponses[concurrentResponses.length - 1].diff + responseDelay; | ||||||||||||||||||||||||||||||
queuedResponses.forEach(r => assert.isAbove(r.diff, minDiff)); | ||||||||||||||||||||||||||||||
queuedResponses.forEach(r => assert.isAtLeast(r.diff, minDiff)); | ||||||||||||||||||||||||||||||
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. was this actually causing a failure? i think your change is more technically correct, but i'm wondering if you actually got a test run where the timers were so perfect that 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 can confirm that I've run into the issue of getting a test run where the timers matched up and a false positive was thrown--just once, but it has happened. 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. yes, I added this fix to the PR, because the build of my first commit broke accidentally |
||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
|
@@ -403,13 +413,15 @@ describe('WebClient', function () { | |||||||||||||||||||||||||||||
assert.lengthOf(responses, 2); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// verify that maxRequestConcurrency requets were all sent concurrently | ||||||||||||||||||||||||||||||
const concurrentResponses = responses.slice(0, 1); // the first 3 responses | ||||||||||||||||||||||||||||||
const concurrentResponses = responses.slice(0, 1); // the first response | ||||||||||||||||||||||||||||||
concurrentResponses.forEach(r => assert.isBelow(r.diff, responseDelay)); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// verify that any requests after maxRequestConcurrency were delayed by the responseDelay | ||||||||||||||||||||||||||||||
const queuedResponses = responses.slice(1); | ||||||||||||||||||||||||||||||
const queuedResponses = responses.slice(1);// the second response | ||||||||||||||||||||||||||||||
const minDiff = concurrentResponses[concurrentResponses.length - 1].diff + responseDelay; | ||||||||||||||||||||||||||||||
queuedResponses.forEach(r => assert.isAbove(r.diff, minDiff)); | ||||||||||||||||||||||||||||||
queuedResponses.forEach(r => { | ||||||||||||||||||||||||||||||
assert.isAtLeast(r.diff, minDiff) | ||||||||||||||||||||||||||||||
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 think this could fit in a single line rather than a block. |
||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
|
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.
still not asserting that the
filename
field exists, and removed the comment that would remind us to improve the tests so that it is asserted. why?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.
sorry, I've overseen this assertion statement and felt that the fix solved the problem pointed in the comment. I'll bring your TODO and assertion back.