diff --git a/.github/workflows/on-pull-request.yml b/.github/workflows/on-pull-request.yml new file mode 100644 index 0000000..63254ea --- /dev/null +++ b/.github/workflows/on-pull-request.yml @@ -0,0 +1,29 @@ +name: Test Suite + +on: + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x, 22.x] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: yarn + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Run test suite + run: yarn test diff --git a/README.md b/README.md index 1eaa7f8..6dcdbfb 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ Example usage: ```js var strava = require('strava-v3'); -strava.athletes.get({id:12345},function(err,payload,limits) { +strava.athlete.get({id:12345},function(err,payload,limits) { //do something with your payload, track rate limits }); ``` @@ -256,13 +256,11 @@ See Strava API docs for returned data structures. * `strava.athlete.get(args,done)` * `strava.athlete.update(args,done)` // only 'weight' can be updated. * `strava.athlete.listActivities(args,done)` *Get list of activity summaries* -* `strava.athlete.listRoutes(args,done)` * `strava.athlete.listClubs(args,done)` * `strava.athlete.listZones(args,done)` #### Athletes -* `strava.athletes.get(args,done)` *Get a single activity. args.id is required* * `strava.athletes.stats(args,done)` #### Activities @@ -270,12 +268,10 @@ See Strava API docs for returned data structures. * `strava.activities.get(args,done)` * `strava.activities.create(args,done)` * `strava.activities.update(args,done)` -* `strava.activities.listFriends(args,done)` -> deprecated at 2.2.0 * `strava.activities.listZones(args,done)` * `strava.activities.listLaps(args,done)` * `strava.activities.listComments(args,done)` * `strava.activities.listKudos(args,done)` -* `strava.activities.listPhotos(args,done)` -> deprecated at 2.2.0 #### Clubs @@ -380,14 +376,16 @@ This update maintains feature parity with the previous implementation of `reques ## Development This package includes a full test suite runnable via `yarn test`. -It will both lint and run shallow tests on API endpoints. +It will both lint and run tests on API endpoints. ### Running the tests -You'll first need to supply `data/strava_config` with an `access_token` that +Many unit tests now use nock to mock the Strava API and can run without any real credentials. +However, some integration-style tests still expect a real token and account data. + +If you want to run the full test suite (including integration tests), you'll need to supply `data/strava_config` with an `access_token` that has both private read and write permissions. Look in `./scripts` for a tool -to help generate this token. Going forward we plan to more testing with a mocked -version of the Strava API so testing with real account credentials are not required. +to help generate this token. * Make sure you've filled out all the fields in `data/strava_config`. * Use `strava.oauth.getRequestAccessURL({scope:"view_private,write"})` to generate the request url and query it via your browser. @@ -408,15 +406,17 @@ data in the account: * Must have created at least one route * Most recent activity with an achievement should also contain a segment -(Contributions to make the test suite more self-contained and robust by converting more tests -to use `nock` are welcome!) +(Parts of the test suite already use `nock` to mock the API. Contributions to convert remaining integration tests to mocks are welcome.) -* You're done! Paste the new `access_token` to `data/strava_config` and go run some tests: +You're done! Paste the new `access_token` to `data/strava_config` and run the full tests: `yarn test`. ### How the tests work +- Tests use Mocha and Should.js. +- HTTP interaction is performed with Axios; tests that mock HTTP use `nock`. + Using the provided `access_token` tests will access each endpoint individually: * (For all `GET` endpoints) checks to ensure the correct type has been returned from the Strava. diff --git a/axiosUtility.js b/axiosUtility.js index 041001b..3dbb17e 100644 --- a/axiosUtility.js +++ b/axiosUtility.js @@ -47,7 +47,7 @@ const httpRequest = async (options) => { data: options.body, // For request body responseType: options.responseType || 'json', // Support different response types maxRedirects: options.maxRedirects || 5, // Set max redirects - validateStatus: options.simple === false ? () => true : undefined // Handle 'simple' option + validateStatus: options.simple === false ? () => true : (status) => status >= 200 && status < 300 // Handle 'simple' option }) return response.data } catch (error) { diff --git a/index.d.ts b/index.d.ts index f38de1e..6c3166e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -100,11 +100,6 @@ export interface GearRoutes { get(args: any, done?: Callback): Promise; } -export interface RunningRacesRoutes { - get(args: any, done?: Callback): Promise; - listRaces(args: any, done?: Callback): Promise; -} - export interface ClubsRoutes { get(args: ClubsRoutesArgs, done?: Callback): Promise; listMembers(args: ClubsRoutesListArgs, done?: Callback): Promise; @@ -350,7 +345,6 @@ export interface Strava { streams: StreamsRoutes; uploads: UploadsRoutes; rateLimiting: RateLimiting; - runningRaces: RunningRacesRoutes; routes: RoutesRoutes; oauth: OAuthRoutes; } diff --git a/index.js b/index.js index ff40c78..561c0d4 100644 --- a/index.js +++ b/index.js @@ -12,7 +12,6 @@ const SegmentEfforts = require('./lib/segmentEfforts') const Streams = require('./lib/streams') const Uploads = require('./lib/uploads') const rateLimiting = require('./lib/rateLimiting') -const RunningRaces = require('./lib/runningRaces') const Routes = require('./lib/routes') const PushSubscriptions = require('./lib/pushSubscriptions') const { axiosInstance, httpRequest } = require('./axiosUtility') @@ -49,7 +48,6 @@ strava.client = function (token, request = httpRequest) { this.streams = new Streams(httpClient) this.uploads = new Uploads(httpClient) this.rateLimiting = rateLimiting - this.runningRaces = new RunningRaces(httpClient) this.routes = new Routes(httpClient) // No Push subscriptions on the client object because they don't use OAuth. } @@ -77,7 +75,6 @@ strava.segmentEfforts = new SegmentEfforts(strava.defaultHttpClient) strava.streams = new Streams(strava.defaultHttpClient) strava.uploads = new Uploads(strava.defaultHttpClient) strava.rateLimiting = rateLimiting -strava.runningRaces = new RunningRaces(strava.defaultHttpClient) strava.routes = new Routes(strava.defaultHttpClient) strava.pushSubscriptions = new PushSubscriptions(strava.defaultHttpClient) diff --git a/lib/athlete.js b/lib/athlete.js index 3817fb8..ebf86e3 100644 --- a/lib/athlete.js +++ b/lib/athlete.js @@ -28,9 +28,6 @@ athlete.prototype.listActivities = async function (args) { athlete.prototype.listClubs = async function (args) { return await this._listHelper('clubs', args) } -athlete.prototype.listRoutes = async function (args) { - return await this._listHelper('routes', args) -} athlete.prototype.listZones = async function (args) { return await this._listHelper('zones', args) } diff --git a/lib/athletes.js b/lib/athletes.js index eda3fe2..c01df90 100644 --- a/lib/athletes.js +++ b/lib/athletes.js @@ -3,9 +3,6 @@ var athletes = function (client) { } //= ==== athletes endpoint ===== -athletes.prototype.get = function (args, done) { - return this._listHelper('', args, done) -} athletes.prototype.stats = function (args, done) { return this._listHelper('stats', args, done) } diff --git a/lib/httpClient.js b/lib/httpClient.js index e3c6681..3a0267f 100644 --- a/lib/httpClient.js +++ b/lib/httpClient.js @@ -188,19 +188,18 @@ HttpClient.prototype._requestHelper = async function (options) { } if (typeof response === 'string') { - // parse the raw JSON string with big-integer reviver for 16+ digit numbers - return JSON.parse(response, (key, value) => { - if (options.responseType === 'json' || options.responseType === 'formdata') { - return JSON.parse(response, (key, value) => { - if (typeof value === 'string' && /^\d{16,}$/.test(value)) { - return JSONbig.parse(value) - } - return value - }) - } else { - return response - } - }) + // If responseType is 'text', return the raw string + if (options.responseType === 'text') { + return response + } + + // For json or formdata, parse the raw JSON string with big-integer reviver for 16+ digit numbers + if (options.responseType === 'json' || options.responseType === 'formdata') { + return JSONbig.parse(response) + } + + // Default: return as-is + return response } // response is not a string or object return it diff --git a/lib/runningRaces.js b/lib/runningRaces.js deleted file mode 100644 index 1486e9a..0000000 --- a/lib/runningRaces.js +++ /dev/null @@ -1,28 +0,0 @@ -var runningRaces = function (client) { - this.client = client -} - -var _qsAllowedProps = [ - 'year' -] - -runningRaces.prototype.get = function (args, done) { - var endpoint = 'running_races/' - - // require running race id - if (typeof args.id === 'undefined') { - throw new Error('args must include an race id') - } - - endpoint += args.id - return this.client.getEndpoint(endpoint, args, done) -} - -runningRaces.prototype.listRaces = function (args, done) { - var qs = this.client.getQS(_qsAllowedProps, args) - var endpoint = 'running_races?' + qs - - return this.client.getEndpoint(endpoint, args, done) -} - -module.exports = runningRaces diff --git a/lib/streams.js b/lib/streams.js index 2196b65..58bf78b 100644 --- a/lib/streams.js +++ b/lib/streams.js @@ -3,6 +3,9 @@ var streams = function (client) { } var _qsAllowedProps = [ + 'keys', + 'key_by_type', + 'original_size', 'resolution', 'series_type' ] @@ -31,18 +34,14 @@ streams.prototype.route = function (args, done) { //= ==== helpers ===== streams.prototype._typeHelper = function (endpoint, args, done) { - var qs = this.client.getQS(_qsAllowedProps, args) - // require id if (typeof args.id === 'undefined') { throw new Error('args must include an id') } - // require types - if (typeof args.types === 'undefined') { - throw new Error('args must include types') - } - endpoint += '/' + args.id + '/streams/' + args.types + '?' + qs + const qs = this.client.getQS(_qsAllowedProps, args) + + endpoint += '/' + args.id + '/streams' + '?' + qs return this.client.getEndpoint(endpoint, args, done) } //= ==== helpers ===== diff --git a/lib/uploads.js b/lib/uploads.js index 56ec42b..e768ed6 100644 --- a/lib/uploads.js +++ b/lib/uploads.js @@ -1,4 +1,4 @@ -const { setTimeout } = require('timers/promises'); +const { setTimeout } = require('timers/promises') var uploads = function (client) { this.client = client @@ -46,7 +46,6 @@ uploads.prototype.post = async function (args) { access_token: args.access_token } return await self._check(checkArgs, args.statusCallback) - } uploads.prototype._check = async function (args, cb) { diff --git a/package.json b/package.json index a656f77..865d6d1 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ ], "author": "austin brown (http://austinjamesbrown.com/)", "contributors": [ - "Mark Stosberg " + "Mark Stosberg ", + "Wesley Schlenker " ], "license": "MIT", "bugs": { @@ -31,27 +32,18 @@ "json-bigint": "^1.0.0" }, "devDependencies": { - "env-restorer": "^1.0.0", - "es6-promise": "^3.3.1", "eslint": "^8.57.1", "eslint-config-standard": "^12.0.0", "eslint-plugin-import": "^2.32.0", "eslint-plugin-node": "^9.2.0", "eslint-plugin-promise": "^4.3.1", "eslint-plugin-standard": "^4.1.0", - "inquirer": "^7.3.3", - "mocha": "^9.2.2", - "mock-fs": "^4.14.0", + "mocha": "11.7.4", "nock": "^11.9.1", - "should": "^13.2.3", - "sinon": "^1.17.7", - "yargs": "^17.7.2" + "tmp": "^0.2.5" }, "mocha": { - "globals": [ - "should" - ], - "timeout": 20000, + "timeout": 5000, "checkLeaks": true, "ui": "bdd", "reporter": "spec" diff --git a/scripts/generate-access-token.js b/scripts/generate-access-token.js index 5923b90..72424eb 100755 --- a/scripts/generate-access-token.js +++ b/scripts/generate-access-token.js @@ -1,85 +1,110 @@ #!/usr/bin/env node -const inquirer = require('inquirer') +const readline = require('readline') const fs = require('fs') -const argv = require('yargs').argv const stravaConfig = './data/strava_config' const stravaConfigTemplate = './strava_config' const stravaApiUrl = 'https://www.strava.com/settings/api#_=_' const strava = require('../index.js') -/** - * Generates the token to access the strava application - */ -console.log('Before processing, you shall fill your strava config with client id and secret provided by Strava:\n' + stravaApiUrl) - -inquirer - .prompt( - [ - { - type: 'input', - name: 'clientId', - message: 'What is your strava client id?', - default: argv['client-id'] - }, - { - type: 'input', - name: 'clientSecret', - message: 'What is your strava client secret?', - default: argv['client-secret'] +// Parse CLI arguments +function parseArgs() { + const args = {} + for (let i = 2; i < process.argv.length; i++) { + if (process.argv[i].startsWith('--')) { + const key = process.argv[i].substring(2) + const value = process.argv[i + 1] + if (value && !value.startsWith('--')) { + args[key] = value + i++ + } else { + args[key] = true } - ]) - .then(function (answers) { - // We copy the strava config file - try { - fs.mkdirSync('data') - } catch (e) { - // nothing } + } + return args +} - var content = fs.readFileSync(stravaConfigTemplate) - fs.writeFileSync(stravaConfig, content) - - // We open the default config file and inject the client_id and client secret - // Without these informations in the config file the getRequestAccessURL would fail - content = fs.readFileSync(stravaConfig) - var config = JSON.parse(content) - config.client_id = answers.clientId - config.client_secret = answers.clientSecret - config.access_token = 'to define' - // You may need to make your callback URL - // at Strava /settings/api temporarily match this - config.redirect_uri = 'http://localhost' - - // We update the config file - fs.writeFileSync(stravaConfig, JSON.stringify(config)) +const argv = parseArgs() + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}) - // Generates the url to have full access - var url = strava.oauth.getRequestAccessURL({ - scope: 'activity:write,profile:write,read_all,profile:read_all,activity:read_all' - }) - // We have to grab the code manually in the browser and then copy/paste it into strava_config as "access_token" - console.log('Connect to the following url and copy the code: ' + url) - - inquirer.prompt( - [ - { - type: 'input', - name: 'code', - message: 'Enter the code obtained from previous strava url (the code parameter in redirection url)' - } - ]) - .then(function (answers2) { - if (!answers2.code) { - console.log('no code provided') - process.exit() - } - strava.oauth.getToken(answers2.code).then(result => { - // We update the access token in strava conf file - if (result.access_token === undefined) throw new Error('Problem with provided code: ' + JSON.stringify(result)) - config.access_token = result.access_token - fs.writeFileSync(stravaConfig, JSON.stringify(config)) - }) - }) - .then(done => console.log('Done. Details written to data/strava_config.')) +function question(prompt) { + return new Promise(resolve => { + rl.question(prompt, resolve) }) +} + +async function main() { + /** + * Generates the token to access the strava application + */ + console.log('Before processing, you shall fill your strava config with client id and secret provided by Strava:\n' + stravaApiUrl) + + const clientId = await question('What is your strava client id? ' + (argv['client-id'] ? `[${argv['client-id']}] ` : '')) + const clientSecret = await question('What is your strava client secret? ' + (argv['client-secret'] ? `[${argv['client-secret']}] ` : '')) + + const finalClientId = clientId || argv['client-id'] + const finalClientSecret = clientSecret || argv['client-secret'] + + // We copy the strava config file + try { + fs.mkdirSync('data') + } catch (e) { + // nothing + } + + var content = fs.readFileSync(stravaConfigTemplate) + fs.writeFileSync(stravaConfig, content) + + // We open the default config file and inject the client_id and client secret + // Without these informations in the config file the getRequestAccessURL would fail + content = fs.readFileSync(stravaConfig) + var config = JSON.parse(content) + config.client_id = finalClientId + config.client_secret = finalClientSecret + config.access_token = 'to define' + // You may need to make your callback URL + // at Strava /settings/api temporarily match this + config.redirect_uri = 'http://localhost' + + // We update the config file + fs.writeFileSync(stravaConfig, JSON.stringify(config)) + + // Generates the url to have full access + var url = strava.oauth.getRequestAccessURL({ + scope: 'activity:write,profile:write,read_all,profile:read_all,activity:read_all' + }) + // We have to grab the code manually in the browser and then copy/paste it into strava_config as "access_token" + console.log('Connect to the following url and copy the code: ' + url) + + const code = await question('Enter the code obtained from previous strava url (the code parameter in redirection url): ') + + if (!code) { + console.log('no code provided') + rl.close() + process.exit() + } + + try { + const result = await strava.oauth.getToken(code) + // We update the access token in strava conf file + if (result.access_token === undefined) throw new Error('Problem with provided code: ' + JSON.stringify(result)) + config.access_token = result.access_token + fs.writeFileSync(stravaConfig, JSON.stringify(config)) + console.log('Done. Details written to data/strava_config.') + } catch (error) { + console.error('Error:', error.message) + } + + rl.close() +} + +main().catch(error => { + console.error('Error:', error.message) + rl.close() + process.exit(1) +}) diff --git a/test/_helper.js b/test/_helper.js index 608bc75..c3ad5f7 100644 --- a/test/_helper.js +++ b/test/_helper.js @@ -1,8 +1,21 @@ -var fs = require('fs') -var strava = require('../') +const strava = require('../') +const authenticator = require('../lib/authenticator') var testsHelper = {} +testsHelper.setupMockAuth = function () { + strava.config({ + access_token: 'test_token', + client_id: 'test_id', + client_secret: 'test_secret', + redirect_uri: 'http://localhost' + }) +} + +testsHelper.cleanupAuth = function () { + authenticator.purge() +} + testsHelper.getSampleAthlete = async function () { return await strava.athlete.get({}) } @@ -16,7 +29,7 @@ testsHelper.getSampleActivity = function (done) { // If we find an activity with an achievement, there's a better chance // that it contains a segment. // This is necessary for getSampleSegment, which uses this function. - function hasAchievement(activity) { return activity.achievement_count > 1 } + function hasAchievement (activity) { return activity.achievement_count > 1 } var withSegment = payload.filter(hasAchievement)[0] @@ -86,13 +99,4 @@ testsHelper.getSampleRunningRace = function (done) { }) } -testsHelper.getAccessToken = function () { - try { - var config = fs.readFileSync('data/strava_config', { encoding: 'utf-8' }) - return JSON.parse(config).access_token - } catch (e) { - return process.env.STRAVA_ACCESS_TOKEN - } -} - module.exports = testsHelper diff --git a/test/activities.js b/test/activities.js index 8620da9..98aff4e 100644 --- a/test/activities.js +++ b/test/activities.js @@ -1,23 +1,26 @@ -var should = require('should') -var sinon = require('sinon') -var strava = require('../') -var testHelper = require('./_helper') -var authenticator = require('../lib/authenticator') +const assert = require('assert') +const nock = require('nock') +const strava = require('../') +const testHelper = require('./_helper') -var testActivity = {} +let testActivity = {} describe('activities_test', function () { - before(function (done) { - testHelper.getSampleActivity(function (err, sampleActivity) { - if (err) { return done(err) } + beforeEach(function () { + nock.cleanAll() + testHelper.setupMockAuth() + // Set a default test activity to use in tests + testActivity = { id: 123456789, resource_state: 3, name: 'Sample Activity' } + }) - done() - }) + afterEach(function () { + nock.cleanAll() + testHelper.cleanupAuth() }) describe('#create()', function () { - it('should create an activity', function (done) { - var args = { + it('should create an activity', async function () { + const args = { name: 'Most Epic Ride EVER!!!', elapsed_time: 18373, distance: 1557840, @@ -25,96 +28,124 @@ describe('activities_test', function () { type: 'Ride' } - strava.activities.create(args, function (err, payload) { - if (!err) { - testActivity = payload; - (payload.resource_state).should.be.exactly(3) - } else { - console.log(err) - } + // Mock the create activity API call + nock('https://www.strava.com') + .post('/api/v3/activities') + .matchHeader('authorization', /Bearer .+/) + .once() + .reply(201, { + id: 987654321, + resource_state: 3, + name: 'Most Epic Ride EVER!!!', + elapsed_time: 18373, + distance: 1557840, + start_date_local: '2013-10-23T10:02:13Z', + type: 'Ride' + }) - done() - }) + const payload = await strava.activities.create(args) + testActivity = payload + assert.strictEqual(payload.resource_state, 3) }) }) describe('#get()', function () { - it('should return information about the corresponding activity', function (done) { - strava.activities.get({ id: testActivity.id }, function (err, payload) { - if (!err) { - (payload.resource_state).should.be.exactly(3) - } else { - console.log(err) - } + it('should return information about the corresponding activity', async function () { + // Mock the get activity API call + nock('https://www.strava.com') + .get('/api/v3/activities/' + testActivity.id) + .query(true) + .matchHeader('authorization', /Bearer .+/) + .once() + .reply(200, { + id: testActivity.id, + resource_state: 3 + }) - done() - }) + const payload = await strava.activities.get({ id: testActivity.id }) + assert.strictEqual(payload.resource_state, 3) }) - it('should return information about the corresponding activity (Promise API)', function () { - return strava.activities.get({ id: testActivity.id }) - .then(function (payload) { - (payload.resource_state).should.be.exactly(3) + it('should return information about the corresponding activity (Promise API)', async function () { + // Mock the get activity API call + nock('https://www.strava.com') + .get('/api/v3/activities/' + testActivity.id) + .query(true) + .matchHeader('authorization', /Bearer .+/) + .once() + .reply(200, { + id: testActivity.id, + resource_state: 3 }) + + const payload = await strava.activities.get({ id: testActivity.id }) + assert.strictEqual(payload.resource_state, 3) }) - it('should work with a specified access token', function (done) { - var token = testHelper.getAccessToken() - var tokenStub = sinon.stub(authenticator, 'getToken', function () { - return undefined - }) + it('should work with a specified access token', async function () { + const token = 'mock-access-token-12345' - strava.activities.get({ - id: testActivity.id, - access_token: token - }, function (err, payload) { - should(err).be.null(); - (payload.resource_state).should.be.exactly(3) - tokenStub.restore() - done() - }) + // Mock the Strava API endpoint + nock('https://www.strava.com') + .get('/api/v3/activities/' + testActivity.id) + .query(true) + .matchHeader('authorization', 'Bearer ' + token) + .once() + .reply(200, { resource_state: 3 }) + + const payload = await strava.activities.get({ id: testActivity.id, access_token: token }) + assert.ok(payload) + assert.strictEqual(payload.resource_state, 3) }) }) describe('#update()', function () { - it('should update an activity', function (done) { - var name = 'Run like the wind!!' - var args = { + it('should update an activity', async function () { + const name = 'Run like the wind!!' + const args = { id: testActivity.id, name: name } - strava.activities.update(args, function (err, payload) { - if (!err) { - (payload.resource_state).should.be.exactly(3); - (payload.name).should.be.exactly(name) - } else { - console.log(err) - } + // Mock the update activity API call + nock('https://www.strava.com') + .put('/api/v3/activities/' + testActivity.id) + .matchHeader('authorization', /Bearer .+/) + .once() + .reply(200, { + id: testActivity.id, + resource_state: 3, + name: name + }) - done() - }) + const payload = await strava.activities.update(args) + assert.strictEqual(payload.resource_state, 3) + assert.strictEqual(payload.name, name) }) }) describe('#updateSportType()', function () { - it('should update the sport type of an activity', function (done) { - var sportType = 'MountainBikeRide' - var args = { + it('should update the sport type of an activity', async function () { + const sportType = 'MountainBikeRide' + const args = { id: testActivity.id, sportType: sportType } - strava.activities.update(args, function (err, payload) { - if (!err) { - (payload.resource_state).should.be.exactly(3); - (payload.sportType).should.be.exactly(sportType) - } else { - console.log(err) - } + // Mock the update activity API call + nock('https://www.strava.com') + .put('/api/v3/activities/' + testActivity.id) + .matchHeader('authorization', /Bearer .+/) + .once() + .reply(200, { + id: testActivity.id, + resource_state: 3, + sport_type: sportType + }) - done() - }) + const payload = await strava.activities.update(args) + assert.strictEqual(payload.resource_state, 3) + assert.strictEqual(payload.sport_type, sportType) }) }) @@ -123,7 +154,7 @@ describe('activities_test', function () { xit('should list heart rate and power zones relating to activity', function (done) { strava.activities.listZones({ id: testActivity.id }, function (err, payload) { if (!err) { - payload.should.be.instanceof(Array) + assert.ok(Array.isArray(payload)) } else { console.log(err) } @@ -134,44 +165,47 @@ describe('activities_test', function () { }) describe('#listLaps()', function () { - it('should list laps relating to activity', function (done) { - strava.activities.listLaps({ id: testActivity.id }, function (err, payload) { - if (!err) { - payload.should.be.instanceof(Array) - } else { - console.log(err) - } - - done() - }) + it('should list laps relating to activity', async function () { + // Mock the list laps API call + nock('https://www.strava.com') + .get('/api/v3/activities/' + testActivity.id + '/laps') + .query(true) + .matchHeader('authorization', /Bearer .+/) + .once() + .reply(200, []) + + const payload = await strava.activities.listLaps({ id: testActivity.id }) + assert.ok(Array.isArray(payload)) }) }) describe('#listComments()', function () { - it('should list comments relating to activity', function (done) { - strava.activities.listComments({ id: testActivity.id }, function (err, payload) { - if (!err) { - payload.should.be.instanceof(Array) - } else { - console.log(err) - } - - done() - }) + it('should list comments relating to activity', async function () { + // Mock the list comments API call + nock('https://www.strava.com') + .get('/api/v3/activities/' + testActivity.id + '/comments') + .query(true) + .matchHeader('authorization', /Bearer .+/) + .once() + .reply(200, []) + + const payload = await strava.activities.listComments({ id: testActivity.id }) + assert.ok(Array.isArray(payload)) }) }) describe('#listKudos()', function () { - it('should list kudos relating to activity', function (done) { - strava.activities.listKudos({ id: testActivity.id }, function (err, payload) { - if (!err) { - payload.should.be.instanceof(Array) - } else { - console.log(err) - } - - done() - }) + it('should list kudos relating to activity', async function () { + // Mock the list kudos API call + nock('https://www.strava.com') + .get('/api/v3/activities/' + testActivity.id + '/kudos') + .query(true) + .matchHeader('authorization', /Bearer .+/) + .once() + .reply(200, []) + + const payload = await strava.activities.listKudos({ id: testActivity.id }) + assert.ok(Array.isArray(payload)) }) }) @@ -180,7 +214,7 @@ describe('activities_test', function () { xit('should list photos relating to activity', function (done) { strava.activities.listPhotos({ id: testActivity.id }, function (err, payload) { if (!err) { - payload.should.be.instanceof(Array) + assert.ok(Array.isArray(payload)) } else { console.log(err) } diff --git a/test/athlete.js b/test/athlete.js index ca56c73..4d423a7 100644 --- a/test/athlete.js +++ b/test/athlete.js @@ -1,64 +1,384 @@ -const should = require('should') +const assert = require('assert') const strava = require('../') +const nock = require('nock') const testHelper = require('./_helper') -describe('athlete_test', function () { +describe('athlete', function () { + beforeEach(function () { + // Clean all nock interceptors before each test to ensure isolation + nock.cleanAll() + testHelper.setupMockAuth() + }) + + afterEach(function () { + nock.cleanAll() + testHelper.cleanupAuth() + }) + describe('#get()', function () { - it('should return detailed athlete information about athlete associated to access_token (level 3)', async () => { + it('should return detailed athlete information about athlete associated to access_token (level 3)', async function () { + const mockAthlete = { + id: 123456, + username: 'testuser', + resource_state: 3, + firstname: 'Test', + lastname: 'User', + city: 'San Francisco', + state: 'California', + country: 'United States', + sex: 'M', + premium: true, + summit: true, + created_at: '2012-01-01T00:00:00Z', + updated_at: '2023-01-01T00:00:00Z', + badge_type_id: 1, + profile_medium: 'https://example.com/medium.jpg', + profile: 'https://example.com/large.jpg', + friend: null, + follower: null, + follower_count: 100, + friend_count: 50, + mutual_friend_count: 0, + athlete_type: 1, + date_preference: '%m/%d/%Y', + measurement_preference: 'feet', + clubs: [], + ftp: 250, + weight: 70, + bikes: [], + shoes: [] + } + + nock('https://www.strava.com') + .get('/api/v3/athlete') + .query(true) + .matchHeader('authorization', 'Bearer test_token') + .once() + .reply(200, mockAthlete) + const response = await strava.athlete.get({}) - response.resource_state.should.be.exactly(3) + + assert.strictEqual(response.resource_state, 3) + assert.strictEqual(response.id, 123456) + assert.strictEqual(response.username, 'testuser') + assert.strictEqual(response.firstname, 'Test') + assert.strictEqual(response.lastname, 'User') + assert.strictEqual(response.city, 'San Francisco') + assert.strictEqual(response.weight, 70) }) }) describe('#listActivities()', function () { - it('should return information about activities associated to athlete with access_token', async () => { - var nowSeconds = Math.floor(Date.now() / 1000) + it('should return information about activities associated to athlete with access_token', async function () { + const mockActivities = [ + { + resource_state: 2, + athlete: { + id: 134815, + resource_state: 1 + }, + name: 'Happy Friday', + distance: 24931.4, + moving_time: 4500, + elapsed_time: 4500, + total_elevation_gain: 0, + type: 'Ride', + sport_type: 'MountainBikeRide', + workout_type: null, + id: 154504250376823, + external_id: 'garmin_push_12345678987654321', + upload_id: 987654321234567900000, + start_date: '2018-05-02T12:15:09Z', + start_date_local: '2018-05-02T05:15:09Z', + timezone: '(GMT-08:00) America/Los_Angeles', + utc_offset: -25200, + start_latlng: null, + end_latlng: null, + location_city: null, + location_state: null, + location_country: 'United States', + achievement_count: 0, + kudos_count: 3, + comment_count: 1, + athlete_count: 1, + photo_count: 0, + map: { + id: 'a12345678987654321', + summary_polyline: null, + resource_state: 2 + }, + device_name: 'Garmin Edge 1030', + trainer: true, + commute: false, + manual: false, + private: false, + flagged: false, + gear_id: 'b12345678987654321', + from_accepted_tag: false, + average_speed: 5.54, + max_speed: 11, + average_cadence: 67.1, + average_watts: 175.3, + weighted_average_watts: 210, + kilojoules: 788.7, + device_watts: true, + has_heartrate: true, + average_heartrate: 140.3, + max_heartrate: 178, + max_watts: 406, + pr_count: 0, + total_photo_count: 1, + has_kudoed: false, + suffer_score: 82 + }, + { + resource_state: 2, + athlete: { + id: 167560, + resource_state: 1 + }, + name: 'Bondcliff', + distance: 23676.5, + moving_time: 5400, + elapsed_time: 5400, + total_elevation_gain: 0, + type: 'Ride', + sport_type: 'MountainBikeRide', + workout_type: null, + id: 1234567809, + external_id: 'garmin_push_12345678987654321', + upload_id: 1234567819, + start_date: '2018-04-30T12:35:51Z', + start_date_local: '2018-04-30T05:35:51Z', + timezone: '(GMT-08:00) America/Los_Angeles', + utc_offset: -25200, + start_latlng: null, + end_latlng: null, + location_city: null, + location_state: null, + location_country: 'United States', + achievement_count: 0, + kudos_count: 4, + comment_count: 0, + athlete_count: 1, + photo_count: 0, + map: { + id: 'a12345689', + summary_polyline: null, + resource_state: 2 + }, + device_name: 'Garmin Edge 1030', + trainer: true, + commute: false, + manual: false, + private: false, + flagged: false, + gear_id: 'b12345678912343', + from_accepted_tag: false, + average_speed: 4.385, + max_speed: 8.8, + average_cadence: 69.8, + average_watts: 200, + weighted_average_watts: 214, + kilojoules: 1080, + device_watts: true, + has_heartrate: true, + average_heartrate: 152.4, + max_heartrate: 183, + max_watts: 403, + pr_count: 0, + total_photo_count: 1, + has_kudoed: false, + suffer_score: 162 + } + ] + + const nowSeconds = Math.floor(Date.now() / 1000) + + nock('https://www.strava.com') + .get('/api/v3/athlete/activities') + .query({ + after: nowSeconds + 3600, + before: nowSeconds + 3600 + }) + .matchHeader('authorization', 'Bearer test_token') + .once() + .reply(200, mockActivities) + const response = await strava.athlete.listActivities({ after: nowSeconds + 3600, before: nowSeconds + 3600 }) - response.should.be.instanceof(Array) + + assert.ok(Array.isArray(response)) + assert.strictEqual(response.length, 2) + assert.strictEqual(response[0].name, 'Happy Friday') + assert.strictEqual(response[0].type, 'Ride') + assert.strictEqual(response[0].sport_type, 'MountainBikeRide') + assert.strictEqual(response[0].distance, 24931.4) + assert.strictEqual(response[1].name, 'Bondcliff') + assert.strictEqual(response[1].athlete_count, 1) }) }) describe('#listClubs()', function () { - it('should return information about clubs associated to athlete with access_token', async () => { + it('should return information about clubs associated to athlete with access_token', async function () { + const mockClubs = [ + { + id: 231407, + resource_state: 2, + name: 'The Strava Club', + profile_medium: 'https://dgalywyr863hv.cloudfront.net/pictures/clubs/231407/5319085/1/medium.jpg', + profile: 'https://dgalywyr863hv.cloudfront.net/pictures/clubs/231407/5319085/1/large.jpg', + cover_photo: 'https://dgalywyr863hv.cloudfront.net/pictures/clubs/231407/5098428/4/large.jpg', + cover_photo_small: 'https://dgalywyr863hv.cloudfront.net/pictures/clubs/231407/5098428/4/small.jpg', + sport_type: 'other', + city: 'San Francisco', + state: 'California', + country: 'United States', + private: false, + member_count: 93151, + featured: false, + verified: true, + url: 'strava' + } + ] + + nock('https://www.strava.com') + .get('/api/v3/athlete/clubs') + .query(true) + .matchHeader('authorization', 'Bearer test_token') + .once() + .reply(200, mockClubs) + const response = await strava.athlete.listClubs({}) - response.should.be.instanceof(Array) - }) - }) - describe('#listRoutes()', function () { - it('should return information about routes associated to athlete with access_token', async () => { - const response = await strava.athlete.listRoutes({}) - response.should.be.instanceof(Array) + assert.ok(Array.isArray(response)) + assert.strictEqual(response.length, 1) + assert.strictEqual(response[0].id, 231407) + assert.strictEqual(response[0].name, 'The Strava Club') + assert.strictEqual(response[0].sport_type, 'other') + assert.strictEqual(response[0].city, 'San Francisco') + assert.strictEqual(response[0].member_count, 93151) + assert.strictEqual(response[0].verified, true) }) }) describe('#listZones()', function () { - it('should return information about heart-rate zones associated to athlete with access_token', async () => { + it('should return information about heart-rate and power zones associated to athlete with access_token', async function () { + const mockZones = [ + { + distribution_buckets: [ + { + max: 0, + min: 0, + time: 1498 + }, + { + max: 50, + min: 0, + time: 62 + }, + { + max: 100, + min: 50, + time: 169 + }, + { + max: 150, + min: 100, + time: 536 + }, + { + max: 200, + min: 150, + time: 672 + }, + { + max: 250, + min: 200, + time: 821 + }, + { + max: 300, + min: 250, + time: 529 + }, + { + max: 350, + min: 300, + time: 251 + }, + { + max: 400, + min: 350, + time: 80 + }, + { + max: 450, + min: 400, + time: 81 + }, + { + max: -1, + min: 450, + time: 343 + } + ], + type: 'power', + resource_state: 3, + sensor_based: true + } + ] + + nock('https://www.strava.com') + .get('/api/v3/athlete/zones') + .query(true) + .matchHeader('authorization', 'Bearer test_token') + .once() + .reply(200, mockZones) + const response = await strava.athlete.listZones({}) - response.should.be.instanceof(Object) + + assert.ok(Array.isArray(response)) + assert.strictEqual(response.length, 1) + assert.strictEqual(response[0].type, 'power') + assert.strictEqual(response[0].resource_state, 3) + assert.strictEqual(response[0].sensor_based, true) + assert.ok(Array.isArray(response[0].distribution_buckets)) + assert.strictEqual(response[0].distribution_buckets.length, 11) + assert.strictEqual(response[0].distribution_buckets[0].time, 1498) + assert.strictEqual(response[0].distribution_buckets[10].min, 450) + assert.strictEqual(response[0].distribution_buckets[10].max, -1) }) }) describe('#update()', function () { - // grab the athlete so we can revert changes - var _athletePreEdit - before(async () => { - _athletePreEdit = await testHelper.getSampleAthlete() - }) + it('should update the weight of the current athlete', async function () { + const weight = 75.5 + const mockUpdatedAthlete = { + id: 123456, + username: 'testuser', + resource_state: 3, + firstname: 'Test', + lastname: 'User', + city: 'San Francisco', + state: 'California', + country: 'United States', + weight: weight + } - it('should update the weight of the current athlete and revert to original', async () => { - var weight = 149 + nock('https://www.strava.com') + .put('/api/v3/athlete', { weight }) + .matchHeader('authorization', 'Bearer test_token') + .once() + .reply(200, mockUpdatedAthlete) const response = await strava.athlete.update({ weight }) - should(response.weight).equal(weight) - // great! we've proven our point, let's reset the athlete data - const updateResponse = await strava.athlete.update({ city: _athletePreEdit.city, weight: _athletePreEdit.weight }) - should(updateResponse.city).equal(_athletePreEdit.city) - should(updateResponse.weight).equal(_athletePreEdit.weight) + assert.strictEqual(response.weight, weight) + assert.strictEqual(response.id, 123456) + assert.strictEqual(response.city, 'San Francisco') }) }) }) diff --git a/test/athletes.js b/test/athletes.js index 9b22e00..e00e0c8 100644 --- a/test/athletes.js +++ b/test/athletes.js @@ -1,41 +1,141 @@ -const should = require('should') -var strava = require('../') -var testHelper = require('./_helper') - -var _sampleAthlete +const assert = require('assert') +const strava = require('../') +const nock = require('nock') +const testHelper = require('./_helper') describe('athletes', function () { - // get the athlete so we have access to an id for testing - before(async () => { - _sampleAthlete = await testHelper.getSampleAthlete() + before(function () { + testHelper.setupMockAuth() }) - describe('#get()', function () { - it('should return basic athlete information (level 2)', function (done) { - strava.athletes.get({ id: _sampleAthlete.id }, function (err, payload) { - if (!err) { - // console.log(payload); - (payload.resource_state).should.be.within(2, 3) - } else { - console.log(err) - } + afterEach(function () { + nock.cleanAll() + }) - done() - }) - }) + after(function () { + testHelper.cleanupAuth() }) -}) -describe('#stats()', function () { - it('should return athlete stats information', function (done) { - strava.athletes.stats({ id: _sampleAthlete.id }, function (err, payload) { - if (!err) { - payload.should.have.property('biggest_ride_distance') - } else { - console.log(err) + describe('#stats()', function () { + it('should return athlete stats information', async function () { + const athleteId = 123456 + const mockResponse = { + biggest_ride_distance: 175454, + biggest_climb_elevation_gain: 1234, + recent_ride_totals: { + count: 3, + distance: 65432, + moving_time: 12345, + elapsed_time: 13579, + elevation_gain: 456, + achievement_count: 2 + }, + recent_run_totals: { + count: 5, + distance: 21098, + moving_time: 5678, + elapsed_time: 5900, + elevation_gain: 123, + achievement_count: 1 + }, + recent_swim_totals: { + count: 0, + distance: 0, + moving_time: 0, + elapsed_time: 0, + elevation_gain: 0, + achievement_count: 0 + }, + ytd_ride_totals: { + count: 45, + distance: 1234567, + moving_time: 234567, + elapsed_time: 250000, + elevation_gain: 12345, + achievement_count: 15 + }, + ytd_run_totals: { + count: 78, + distance: 654321, + moving_time: 123456, + elapsed_time: 130000, + elevation_gain: 3456, + achievement_count: 8 + }, + ytd_swim_totals: { + count: 0, + distance: 0, + moving_time: 0, + elapsed_time: 0, + elevation_gain: 0, + achievement_count: 0 + }, + all_ride_totals: { + count: 523, + distance: 12345678, + moving_time: 2345678, + elapsed_time: 2500000, + elevation_gain: 123456, + achievement_count: 234 + }, + all_run_totals: { + count: 456, + distance: 3456789, + moving_time: 987654, + elapsed_time: 1000000, + elevation_gain: 23456, + achievement_count: 89 + }, + all_swim_totals: { + count: 12, + distance: 24000, + moving_time: 12000, + elapsed_time: 13000, + elevation_gain: 0, + achievement_count: 3 + } } - done() + nock('https://www.strava.com') + .get(`/api/v3/athletes/${athleteId}/stats`) + .query(true) // Accept any query parameters + .matchHeader('authorization', 'Bearer test_token') + .reply(200, mockResponse) + + const payload = await strava.athletes.stats({ id: athleteId }) + + // Test the main structure + assert.strictEqual(typeof payload.biggest_ride_distance, 'number') + assert.strictEqual(typeof payload.biggest_climb_elevation_gain, 'number') + + // Test recent_ride_totals structure + assert.strictEqual(typeof payload.recent_ride_totals, 'object') + assert.strictEqual(typeof payload.recent_ride_totals.count, 'number') + assert.strictEqual(typeof payload.recent_ride_totals.distance, 'number') + assert.strictEqual(typeof payload.recent_ride_totals.moving_time, 'number') + assert.strictEqual(typeof payload.recent_ride_totals.elapsed_time, 'number') + assert.strictEqual(typeof payload.recent_ride_totals.elevation_gain, 'number') + assert.strictEqual(typeof payload.recent_ride_totals.achievement_count, 'number') + + // Test that all expected total categories exist + const totalCategories = ['recent', 'ytd', 'all'] + const activityTypes = ['ride', 'run', 'swim'] + + totalCategories.forEach(category => { + activityTypes.forEach(type => { + const key = `${category}_${type}_totals` + assert.ok(payload.hasOwnProperty(key), `Missing ${key}`) + assert.strictEqual(typeof payload[key], 'object', `${key} should be an object`) + assert.strictEqual(typeof payload[key].count, 'number', `${key}.count should be a number`) + assert.strictEqual(typeof payload[key].distance, 'number', `${key}.distance should be a number`) + }) + }) + + // Test specific values + assert.strictEqual(payload.biggest_ride_distance, 175454) + assert.strictEqual(payload.recent_run_totals.count, 5) + assert.strictEqual(payload.ytd_ride_totals.distance, 1234567) + assert.strictEqual(payload.all_swim_totals.count, 12) }) }) }) diff --git a/test/authenticator.js b/test/authenticator.js index eb2c4b0..b452b08 100644 --- a/test/authenticator.js +++ b/test/authenticator.js @@ -1,128 +1,125 @@ -require('should') -var mockFS = require('mock-fs') -var envRestorer = require('env-restorer') -var authenticator = require('../lib/authenticator') - -// Restore File system mocks, authentication state and environment variables -var restoreAll = function () { - mockFS.restore() - authenticator.purge() - envRestorer.restore() -} +const assert = require('assert') +const authenticator = require('../lib/authenticator') describe('authenticator_test', function () { - describe('#getToken()', function () { - it('should read the access token from the config file', function () { - mockFS({ - 'data/strava_config': JSON.stringify({ - 'access_token': 'abcdefghi', - 'client_id': 'jklmnopqr', - 'client_secret': 'stuvwxyz', - 'redirect_uri': 'https://sample.com' - }) - }) - delete process.env.STRAVA_ACCESS_TOKEN - authenticator.purge(); - - (authenticator.getToken()).should.be.exactly('abcdefghi') + // Store original environment variables + let originalEnv = {} + + beforeEach(function () { + // Save original environment variables + originalEnv = { + STRAVA_ACCESS_TOKEN: process.env.STRAVA_ACCESS_TOKEN, + STRAVA_CLIENT_ID: process.env.STRAVA_CLIENT_ID, + STRAVA_CLIENT_SECRET: process.env.STRAVA_CLIENT_SECRET, + STRAVA_REDIRECT_URI: process.env.STRAVA_REDIRECT_URI + } + // Clear all Strava environment variables to ensure clean state + delete process.env.STRAVA_ACCESS_TOKEN + delete process.env.STRAVA_CLIENT_ID + delete process.env.STRAVA_CLIENT_SECRET + delete process.env.STRAVA_REDIRECT_URI + // Clear authenticator cache + authenticator.purge() + }) + + afterEach(function () { + // Restore environment variables + Object.keys(originalEnv).forEach(key => { + if (originalEnv[key] === undefined) { + delete process.env[key] + } else { + process.env[key] = originalEnv[key] + } }) + // Clear authenticator cache + authenticator.purge() + }) + describe('#getToken()', function () { it('should read the access token from the env vars', function () { - mockFS({ - 'data': {} - }) process.env.STRAVA_ACCESS_TOKEN = 'abcdefghi' - authenticator.purge(); - (authenticator.getToken()).should.be.exactly('abcdefghi') + assert.strictEqual(authenticator.getToken(), 'abcdefghi') }) - afterEach(restoreAll) + it('should return undefined when no token is set', function () { + // No environment variable set + assert.strictEqual(authenticator.getToken(), undefined) + }) }) describe('#getClientId()', function () { - it('should read the client id from the config file', function () { - mockFS({ - 'data/strava_config': JSON.stringify({ - 'access_token': 'abcdefghi', - 'client_id': 'jklmnopqr', - 'client_secret': 'stuvwxyz', - 'redirect_uri': 'https://sample.com' - }) - }) - delete process.env.STRAVA_CLIENT_ID - authenticator.purge(); - - (authenticator.getClientId()).should.be.exactly('jklmnopqr') - }) - it('should read the client id from the env vars', function () { - mockFS({ - 'data': {} - }) - process.env.STRAVA_CLIENT_ID = 'abcdefghi' - authenticator.purge(); + process.env.STRAVA_CLIENT_ID = 'jklmnopqr' - (authenticator.getClientId()).should.be.exactly('abcdefghi') + assert.strictEqual(authenticator.getClientId(), 'jklmnopqr') }) - afterEach(restoreAll) + it('should return undefined and log when no client id is set', function () { + // Capture console.log to verify it's called + const originalLog = console.log + let logCalled = false + console.log = function (msg) { + if (msg === 'No client id found') { + logCalled = true + } + } + + assert.strictEqual(authenticator.getClientId(), undefined) + assert.ok(logCalled, 'Should log "No client id found"') + + // Restore console.log + console.log = originalLog + }) }) describe('#getClientSecret()', function () { - it('should read the client secret from the config file', function () { - mockFS({ - 'data/strava_config': JSON.stringify({ - 'access_token': 'abcdefghi', - 'client_id': 'jklmnopqr', - 'client_secret': 'stuvwxyz', - 'redirect_uri': 'https://sample.com' - }) - }) - delete process.env.STRAVA_CLIENT_SECRET - authenticator.purge(); - - (authenticator.getClientSecret()).should.be.exactly('stuvwxyz') - }) - it('should read the client secret from the env vars', function () { - mockFS({ - 'data': {} - }) - process.env.STRAVA_CLIENT_SECRET = 'abcdefghi' - authenticator.purge(); + process.env.STRAVA_CLIENT_SECRET = 'stuvwxyz' - (authenticator.getClientSecret()).should.be.exactly('abcdefghi') + assert.strictEqual(authenticator.getClientSecret(), 'stuvwxyz') }) - afterEach(restoreAll) - }) - describe('#getRedirectUri()', function () { - it('should read the redirect URI from the config file', function () { - mockFS({ - 'data/strava_config': JSON.stringify({ - 'access_token': 'abcdefghi', - 'client_id': 'jklmnopqr', - 'client_secret': 'stuvwxyz', - 'redirect_uri': 'https://sample.com' - }) - }) - delete process.env.STRAVA_REDIRECT_URI - authenticator.purge(); - - (authenticator.getRedirectUri()).should.be.exactly('https://sample.com') + it('should return undefined and log when no client secret is set', function () { + // Capture console.log to verify it's called + const originalLog = console.log + let logCalled = false + console.log = function (msg) { + if (msg === 'No client secret found') { + logCalled = true + } + } + + assert.strictEqual(authenticator.getClientSecret(), undefined) + assert.ok(logCalled, 'Should log "No client secret found"') + + // Restore console.log + console.log = originalLog }) + }) + describe('#getRedirectUri()', function () { it('should read the redirect URI from the env vars', function () { - mockFS({ - 'data': {} - }) process.env.STRAVA_REDIRECT_URI = 'https://sample.com' - authenticator.purge(); - (authenticator.getRedirectUri()).should.be.exactly('https://sample.com') + assert.strictEqual(authenticator.getRedirectUri(), 'https://sample.com') }) - afterEach(restoreAll) + it('should return undefined and log when no redirect URI is set', function () { + // Capture console.log to verify it's called + const originalLog = console.log + let logCalled = false + console.log = function (msg) { + if (msg === 'No redirectUri found') { + logCalled = true + } + } + + assert.strictEqual(authenticator.getRedirectUri(), undefined) + assert.ok(logCalled, 'Should log "No redirectUri found"') + + // Restore console.log + console.log = originalLog + }) }) }) diff --git a/test/client.js b/test/client.js index e669f2a..2b02a13 100644 --- a/test/client.js +++ b/test/client.js @@ -1,46 +1,102 @@ /* eslint new-cap: 0 */ -require('should') +const assert = require('assert') +const nock = require('nock') const { StatusCodeError } = require('../axiosUtility') const strava = require('../') -const file = require('fs').readFileSync('data/strava_config', 'utf8') -const config = JSON.parse(file) -const token = config.access_token -// Test the "client" API that is based on providing an explicit per-instance access_token -// Rather than the original global-singleton configuration design. - -const client = new strava.client(token) +// Use explicit tokens rather than reading from a file +const GOOD_TOKEN = 'good-test-token' +const BAD_TOKEN = 'bad-test-token' describe('client_test', function () { - // All data fetching methods should work on the client (except Oauth). + afterEach(() => { + // Clean up after each test + nock.cleanAll() + }) + + // All data fetching methods should work on the client (except OAuth). // Try client.athlete.get() as a sample describe('#athlete.get()', function () { - it('Should reject promise with StatusCodeError for non-2xx response', function (done) { - const badClient = new strava.client('BOOM') - badClient.athlete.get({}) - .catch(StatusCodeError, function (e) { - done() + it('Should reject promise with StatusCodeError for non-2xx response', async function () { + // Mock athlete endpoint for BAD token -> 401 + nock('https://www.strava.com') + .get('/api/v3/athlete') + .query(true) // Accept any query parameters + .matchHeader('authorization', 'Bearer ' + BAD_TOKEN) + .once() + .reply(401, { message: 'Authorization Error', errors: [{ resource: 'Application', code: 'invalid' }] }) + + const badClient = new strava.client(BAD_TOKEN) + try { + await badClient.athlete.get({}) + assert.fail('Expected athlete.get to reject with StatusCodeError') + } catch (err) { + assert.ok(err instanceof StatusCodeError) + assert.strictEqual(err.statusCode, 401) + } + }) + + it('should return detailed athlete information about athlete associated to access_token', async function () { + // Mock athlete endpoint for GOOD token -> 200 + nock('https://www.strava.com') + .get('/api/v3/athlete') + .matchHeader('authorization', 'Bearer ' + GOOD_TOKEN) + .once() + .reply(200, { + resource_state: 3, + id: 12345, + firstname: 'Test', + lastname: 'User' }) + + const client = new strava.client(GOOD_TOKEN) + const payload = await client.athlete.get({}) + assert.ok(payload) + assert.strictEqual(payload.resource_state, 3) + assert.strictEqual(payload.id, 12345) }) - it('Callback interface should return StatusCodeError for non-2xx response', function (done) { - const badClient = new strava.client('BOOM') - badClient.athlete.get({}, function (err, payload) { - err.should.be.an.instanceOf(StatusCodeError) - done() - }) + it('Should reject promise with StatusCodeError when using bad token', async function () { + // Mock athlete endpoint for BAD token -> 401 + // Testing with a second interceptor to ensure nock works correctly + nock('https://www.strava.com') + .get('/api/v3/athlete') + .query(true) // Accept any query parameters + .matchHeader('authorization', 'Bearer ' + BAD_TOKEN) + .once() + .reply(401, { message: 'Authorization Error' }) + + const badClient = new strava.client(BAD_TOKEN) + try { + await badClient.athlete.get({}) + assert.fail('Expected athlete.get to reject with StatusCodeError') + } catch (err) { + assert.ok(err instanceof StatusCodeError) + assert.strictEqual(err.statusCode, 401) + assert.strictEqual(err.data.message, 'Authorization Error') + } }) - it('should return detailed athlete information about athlete associated to access_token (level 3)', function (done) { - client.athlete.get({}, function (err, payload) { - if (!err) { - (payload.resource_state).should.be.exactly(3) - } else { - console.log(err) - } + it('Should successfully return athlete data with valid token', async function () { + // Mock athlete endpoint for GOOD token -> 200 + // Testing a second successful request to verify client instances work correctly + nock('https://www.strava.com') + .get('/api/v3/athlete') + .matchHeader('authorization', 'Bearer ' + GOOD_TOKEN) + .once() + .reply(200, { + resource_state: 3, + id: 67890, + firstname: 'Another', + lastname: 'Athlete' + }) - done() - }) + const client = new strava.client(GOOD_TOKEN) + const payload = await client.athlete.get({}) + assert.ok(payload) + assert.strictEqual(payload.resource_state, 3) + assert.strictEqual(payload.id, 67890) + assert.strictEqual(payload.firstname, 'Another') }) }) }) diff --git a/test/clubs.js b/test/clubs.js index 6dd4910..563de40 100644 --- a/test/clubs.js +++ b/test/clubs.js @@ -1,55 +1,196 @@ -require('should') -var strava = require('../') -var testHelper = require('./_helper') +const assert = require('assert') +const strava = require('../') +const nock = require('nock') +const testHelper = require('./_helper') -var _sampleClub - -describe('clubs_test', function () { - before(function (done) { - testHelper.getSampleClub(function (err, payload) { - if (err) { return done(err) } +describe('clubs', function () { + beforeEach(function () { + // Clean all nock interceptors before each test to ensure isolation + nock.cleanAll() + testHelper.setupMockAuth() + }) - _sampleClub = payload - done() - }) + afterEach(function () { + nock.cleanAll() + testHelper.cleanupAuth() }) describe('#get()', function () { - it('should return club detailed information', function (done) { - strava.clubs.get({ id: _sampleClub.id }, function (err, payload) { - if (!err) { - (payload.resource_state).should.be.exactly(3) - } else { - console.log(err) - } - done() - }) + it('should return club detailed information', async function () { + const clubId = 1 + const mockClub = { + id: clubId, + resource_state: 3, + name: 'Team Strava Cycling', + profile_medium: 'https://dgalywyr863hv.cloudfront.net/pictures/clubs/1/1582/4/medium.jpg', + profile: 'https://dgalywyr863hv.cloudfront.net/pictures/clubs/1/1582/4/large.jpg', + cover_photo: 'https://dgalywyr863hv.cloudfront.net/pictures/clubs/1/4328276/1/large.jpg', + cover_photo_small: 'https://dgalywyr863hv.cloudfront.net/pictures/clubs/1/4328276/1/small.jpg', + sport_type: 'cycling', + activity_types: ['Ride', 'VirtualRide', 'EBikeRide', 'Velomobile', 'Handcycle'], + city: 'San Francisco', + state: 'California', + country: 'United States', + private: true, + member_count: 116, + featured: false, + verified: false, + url: 'team-strava-bike', + membership: 'member', + admin: false, + owner: false, + description: 'Private club for Cyclists who work at Strava.', + club_type: 'company', + post_count: 29, + owner_id: 759, + following_count: 107 + } + + nock('https://www.strava.com') + .get(`/api/v3/clubs/${clubId}`) + .query(true) + .matchHeader('authorization', 'Bearer test_token') + .once() + .reply(200, mockClub) + + const payload = await strava.clubs.get({ id: clubId }) + + assert.strictEqual(payload.resource_state, 3) + assert.strictEqual(payload.id, clubId) + assert.strictEqual(payload.name, 'Team Strava Cycling') + assert.strictEqual(payload.sport_type, 'cycling') + assert.strictEqual(payload.city, 'San Francisco') + assert.strictEqual(payload.state, 'California') + assert.strictEqual(payload.country, 'United States') + assert.strictEqual(payload.member_count, 116) + assert.strictEqual(payload.private, true) + assert.ok(Array.isArray(payload.activity_types)) + assert.strictEqual(payload.activity_types.length, 5) + assert.ok(payload.activity_types.includes('Ride')) + assert.strictEqual(payload.club_type, 'company') + assert.strictEqual(payload.owner_id, 759) }) }) describe('#listMembers()', function () { - it('should return a summary list of athletes in club', function (done) { - strava.clubs.listMembers({ id: _sampleClub.id }, function (err, payload) { - if (!err) { - payload.should.be.instanceof(Array) - } else { - console.log(err) + it('should return a summary list of athletes in club', async function () { + const clubId = 1 + const mockMembers = [ + { + resource_state: 2, + firstname: 'John', + lastname: 'Doe', + membership: 'member', + admin: false, + owner: false + }, + { + resource_state: 2, + firstname: 'Jane', + lastname: 'Smith', + membership: 'member', + admin: true, + owner: false + }, + { + resource_state: 2, + firstname: 'Bob', + lastname: 'Johnson', + membership: 'member', + admin: false, + owner: true } - done() - }) + ] + + nock('https://www.strava.com') + .get(`/api/v3/clubs/${clubId}/members`) + .query(true) + .matchHeader('authorization', 'Bearer test_token') + .once() + .reply(200, mockMembers) + + const payload = await strava.clubs.listMembers({ id: clubId }) + + assert.ok(Array.isArray(payload)) + assert.strictEqual(payload.length, 3) + assert.strictEqual(payload[0].resource_state, 2) + assert.strictEqual(payload[0].firstname, 'John') + assert.strictEqual(payload[0].lastname, 'Doe') + assert.strictEqual(payload[1].admin, true) + assert.strictEqual(payload[2].owner, true) }) }) describe('#listActivities()', function () { - it('should return a list of club activities', function (done) { - strava.clubs.listActivities({ id: _sampleClub.id }, function (err, payload) { - if (!err) { - payload.should.be.instanceof(Array) - } else { - console.log(err) + it('should return a list of club activities', async function () { + const clubId = 1 + const mockActivities = [ + { + resource_state: 2, + athlete: { + resource_state: 2, + firstname: 'Peter', + lastname: 'S.' + }, + name: 'World Championship', + distance: 2641.7, + moving_time: 577, + elapsed_time: 635, + total_elevation_gain: 8.8, + type: 'Ride', + sport_type: 'MountainBikeRide', + workout_type: null + }, + { + resource_state: 2, + athlete: { + resource_state: 2, + firstname: 'Maria', + lastname: 'K.' + }, + name: 'Morning Run', + distance: 5234.2, + moving_time: 1823, + elapsed_time: 1900, + total_elevation_gain: 45.3, + type: 'Run', + sport_type: 'Run', + workout_type: null } - done() - }) + ] + + nock('https://www.strava.com') + .get(`/api/v3/clubs/${clubId}/activities`) + .query(true) + .matchHeader('authorization', 'Bearer test_token') + .once() + .reply(200, mockActivities) + + const payload = await strava.clubs.listActivities({ id: clubId }) + + assert.ok(Array.isArray(payload)) + assert.strictEqual(payload.length, 2) + + // Check first activity + assert.strictEqual(payload[0].resource_state, 2) + assert.strictEqual(payload[0].name, 'World Championship') + assert.strictEqual(payload[0].distance, 2641.7) + assert.strictEqual(payload[0].moving_time, 577) + assert.strictEqual(payload[0].elapsed_time, 635) + assert.strictEqual(payload[0].total_elevation_gain, 8.8) + assert.strictEqual(payload[0].type, 'Ride') + assert.strictEqual(payload[0].sport_type, 'MountainBikeRide') + assert.strictEqual(payload[0].workout_type, null) + assert.ok(payload[0].athlete) + assert.strictEqual(payload[0].athlete.firstname, 'Peter') + assert.strictEqual(payload[0].athlete.lastname, 'S.') + + // Check second activity + assert.strictEqual(payload[1].name, 'Morning Run') + assert.strictEqual(payload[1].type, 'Run') + assert.strictEqual(payload[1].sport_type, 'Run') + assert.ok(payload[1].athlete) + assert.strictEqual(payload[1].athlete.firstname, 'Maria') }) }) }) diff --git a/test/config.js b/test/config.js index b58f574..8961753 100644 --- a/test/config.js +++ b/test/config.js @@ -1,4 +1,4 @@ -require('should') +const assert = require('assert') const strava = require('../') const authenticator = require('../lib/authenticator') @@ -10,12 +10,12 @@ describe('config_test', function () { 'client_id': 'exlmnopqr', 'client_secret': 'exuvwxyz', 'redirect_uri': 'https://sample.com/explicit' - }); + }) - (authenticator.getToken()).should.be.exactly('excdefghi'); - (authenticator.getClientId()).should.be.exactly('exlmnopqr'); - (authenticator.getClientSecret()).should.be.exactly('exuvwxyz'); - (authenticator.getRedirectUri()).should.be.exactly('https://sample.com/explicit') + assert.strictEqual(authenticator.getToken(), 'excdefghi') + assert.strictEqual(authenticator.getClientId(), 'exlmnopqr') + assert.strictEqual(authenticator.getClientSecret(), 'exuvwxyz') + assert.strictEqual(authenticator.getRedirectUri(), 'https://sample.com/explicit') authenticator.purge() }) }) diff --git a/test/gear.js b/test/gear.js index 1e3ef56..59411c7 100644 --- a/test/gear.js +++ b/test/gear.js @@ -1,30 +1,50 @@ -require('should') -var strava = require('../') -var testHelper = require('./_helper') - -var _sampleGear +const assert = require('assert') +const strava = require('../') +const nock = require('nock') +const testHelper = require('./_helper') describe('gear_test', function () { - before(function (done) { - testHelper.getSampleGear(function (err, payload) { - if (err) { return done(err) } - - _sampleGear = payload + before(function () { + testHelper.setupMockAuth() + }) - if (!_sampleGear || !_sampleGear.id) { return done(new Error('At least one piece of gear posted to Strava is required for testing.')) } + afterEach(function () { + nock.cleanAll() + }) - done() - }) + after(function () { + testHelper.cleanupAuth() }) describe('#get()', function () { - it('should return detailed athlete information about gear (level 3)', function (done) { - strava.gear.get({ id: _sampleGear.id }, function (err, payload) { - if (err) { return done(err) } + it('should return detailed athlete information about gear (level 3)', async function () { + const gearId = 'b1231' + const mockResponse = { + id: 'b1231', + primary: false, + resource_state: 3, + distance: 388206, + brand_name: 'BMC', + model_name: 'Teammachine', + frame_type: 3, + description: 'My Bike.' + } + + nock('https://www.strava.com') + .get(`/api/v3/gear/${gearId}`) + .matchHeader('authorization', 'Bearer test_token') + .reply(200, mockResponse) + + const payload = await strava.gear.get({ id: gearId }) - (payload.resource_state).should.be.exactly(3) - done() - }) + assert.strictEqual(payload.id, 'b1231') + assert.strictEqual(payload.primary, false) + assert.strictEqual(payload.resource_state, 3) + assert.strictEqual(payload.distance, 388206) + assert.strictEqual(payload.brand_name, 'BMC') + assert.strictEqual(payload.model_name, 'Teammachine') + assert.strictEqual(payload.frame_type, 3) + assert.strictEqual(payload.description, 'My Bike.') }) }) }) diff --git a/test/oauth.js b/test/oauth.js index 790f383..28a3329 100644 --- a/test/oauth.js +++ b/test/oauth.js @@ -1,10 +1,21 @@ -const should = require('should') +const assert = require('assert') const authenticator = require('../lib/authenticator') const querystring = require('querystring') const strava = require('../') const nock = require('nock') +const testHelper = require('./_helper') describe('oauth_test', function () { + beforeEach(function () { + nock.cleanAll() + testHelper.setupMockAuth() + }) + + afterEach(function () { + nock.cleanAll() + testHelper.cleanupAuth() + }) + describe('#getRequestAccessURL()', function () { it('should return the full request access url', function () { const targetUrl = 'https://www.strava.com/oauth/authorize?' + @@ -19,39 +30,35 @@ describe('oauth_test', function () { scope: 'view_private,write' }) - url.should.be.exactly(targetUrl) + assert.strictEqual(url, targetUrl) }) }) describe('#deauthorize()', function () { it('Should have method deauthorize', function () { - strava.oauth.should.have.property('deauthorize') - }) - - it('Should return 401 with invalid token', function (done) { - strava.oauth.deauthorize({ access_token: 'BOOM' }, function (err, payload) { - should(err).be.null() - should(payload).have.property('message').eql('Authorization Error') - done() - }) + assert.ok(typeof strava.oauth.deauthorize === 'function') }) - it('Should return 401 with invalid token (Promise API)', function () { - return strava.oauth.deauthorize({ access_token: 'BOOM' }) - .then(function (payload) { - (payload).should.have.property('message').eql('Authorization Error') + it('Should return 401 with invalid token', async function () { + nock('https://www.strava.com') + .post('/oauth/deauthorize') + .once() + .reply(401, { + message: 'Authorization Error' }) + + const payload = await strava.oauth.deauthorize({ access_token: 'BOOM' }) + assert.ok(payload) + assert.strictEqual(payload.message, 'Authorization Error') }) - // Not sure how to test since we don't have a token that we want to deauthorize }) describe('#getToken()', function () { - before(() => { + it('should return an access_token', async function () { nock('https://www.strava.com') - // .filteringPath(() => '/oauth/token') .post(/^\/oauth\/token/) - // Match requests where this is true in the query string .query(qs => qs.grant_type === 'authorization_code') + .once() .reply(200, { 'token_type': 'Bearer', 'access_token': '987654321234567898765432123456789', @@ -60,42 +67,34 @@ describe('oauth_test', function () { 'expires_at': 1531378346, 'state': 'STRAVA' }) - }) - it('should return an access_token', async () => { const payload = await strava.oauth.getToken() - should(payload).have.property('access_token').eql('987654321234567898765432123456789') + assert.ok(payload) + assert.strictEqual(payload.access_token, '987654321234567898765432123456789') }) }) - describe('#refreshToken()', () => { - before(() => { + describe('#refreshToken()', function () { + it('should return expected response when refreshing token', async function () { nock('https://www.strava.com') - .filteringPath(() => '/oauth/token') .post(/^\/oauth\/token/) - .reply(200, - { - 'access_token': '38c8348fc7f988c39d6f19cf8ffb17ab05322152', - 'expires_at': 1568757689, - 'expires_in': 21432, - 'refresh_token': '583809f59f585bdb5363a4eb2a0ac19562d73f05', - 'token_type': 'Bearer' - } - ) - }) - it('should return expected response when refreshing token', () => { - return strava.oauth.refreshToken('MOCK DOESNT CARE IF THIS IS VALID') - .then(result => { - result.should.eql( - { - 'access_token': '38c8348fc7f988c39d6f19cf8ffb17ab05322152', - 'expires_at': 1568757689, - 'expires_in': 21432, - 'refresh_token': '583809f59f585bdb5363a4eb2a0ac19562d73f05', - 'token_type': 'Bearer' - } - ) + .once() + .reply(200, { + 'access_token': '38c8348fc7f988c39d6f19cf8ffb17ab05322152', + 'expires_at': 1568757689, + 'expires_in': 21432, + 'refresh_token': '583809f59f585bdb5363a4eb2a0ac19562d73f05', + 'token_type': 'Bearer' }) + + const result = await strava.oauth.refreshToken('MOCK DOESNT CARE IF THIS IS VALID') + assert.deepStrictEqual(result, { + 'access_token': '38c8348fc7f988c39d6f19cf8ffb17ab05322152', + 'expires_at': 1568757689, + 'expires_in': 21432, + 'refresh_token': '583809f59f585bdb5363a4eb2a0ac19562d73f05', + 'token_type': 'Bearer' + }) }) }) }) diff --git a/test/pushSubscriptions.js b/test/pushSubscriptions.js index 739ca61..e8427bb 100644 --- a/test/pushSubscriptions.js +++ b/test/pushSubscriptions.js @@ -1,20 +1,32 @@ 'use strict' -var nock = require('nock') -var assert = require('assert') -var should = require('should') -var strava = require('../') +const nock = require('nock') +const assert = require('assert') +const strava = require('../') describe('pushSubscriptions_test', function () { + afterEach(function () { + // Clean up nock interceptors after each test + nock.cleanAll() + }) describe('#list()', function () { - before(() => { + it('should not sent Authorization header to Strava', async () => { nock('https://www.strava.com') - .filteringPath(() => '/api/v3/push_subscriptions/') - .get(/^\/api\/v3\/push_subscriptions/) - // The first reply just echo's the request headers so we can test them. + .get('/api/v3/push_subscriptions') + .query(true) + .once() .reply(200, function (uri, requestBody) { return { headers: this.req.headers } }) - .get(/^\/api\/v3\/push_subscriptions/) + + const result = await strava.pushSubscriptions.list() + assert.ok(!result.headers.authorization) + }) + + it('should return list of subscriptions', async () => { + nock('https://www.strava.com') + .get('/api/v3/push_subscriptions') + .query(true) + .once() .reply(200, [ { 'id': 1, @@ -25,42 +37,44 @@ describe('pushSubscriptions_test', function () { 'updated_at': '2015-04-29T18:11:09.400558047-07:00' } ]) - }) - it('should not sent Authorization header to Strava', () => { - return strava.pushSubscriptions.list() - .then(result => { - should.not.exist(result.headers.authorization) - }) - }) - - it('should return list of subscriptions', () => { - return strava.pushSubscriptions.list() - .then(result => { - result.should.eql([ - { - 'id': 1, - 'object_type': 'activity', - 'aspect_type': 'create', - 'callback_url': 'http://you.com/callback/', - 'created_at': '2015-04-29T18:11:09.400558047-07:00', - 'updated_at': '2015-04-29T18:11:09.400558047-07:00' - } - ]) - }) + const result = await strava.pushSubscriptions.list() + assert.deepStrictEqual(result, [ + { + 'id': 1, + 'object_type': 'activity', + 'aspect_type': 'create', + 'callback_url': 'http://you.com/callback/', + 'created_at': '2015-04-29T18:11:09.400558047-07:00', + 'updated_at': '2015-04-29T18:11:09.400558047-07:00' + } + ]) }) }) describe('#create({callback_url:...})', function () { - before(() => { + it('should throw with no params', () => { + assert.throws(() => strava.pushSubscriptions.create()) + }) + + it('should not sent Authorization header to Strava', async () => { nock('https://www.strava.com') - .filteringPath(() => '/api/v3/push_subscriptions') - // The first reply just echo's the request headers so we can test them. - .post(/^\/api\/v3\/push_subscriptions/) + .post('/api/v3/push_subscriptions') + .once() .reply(200, function (uri, requestBody) { return { headers: this.req.headers } }) - .post(/^\/api\/v3\/push_subscriptions/) + + const result = await strava.pushSubscriptions.create({ + 'callback_url': 'http://you.com/callback/' + }) + assert.ok(!result.headers.authorization) + }) + + it('should return details of created activity', async () => { + nock('https://www.strava.com') + .post('/api/v3/push_subscriptions') + .once() .reply(200, { 'id': 1, 'object_type': 'activity', @@ -69,73 +83,50 @@ describe('pushSubscriptions_test', function () { 'created_at': '2015-04-29T18:11:09.400558047-07:00', 'updated_at': '2015-04-29T18:11:09.400558047-07:00' }) - }) - it('should throw with no params', () => { - assert.throws(() => strava.pushSubscriptions.create()) - }) - - it('should not sent Authorization header to Strava', () => { - return strava.pushSubscriptions.create({ + const result = await strava.pushSubscriptions.create({ 'callback_url': 'http://you.com/callback/' }) - .then(result => { - should.not.exist(result.headers.authorization) - }) - }) - - it('should return details of created activity', () => { - return strava.pushSubscriptions.create({ - 'callback_url': 'http://you.com/callback/' + assert.deepStrictEqual(result, { + 'id': 1, + 'object_type': 'activity', + 'aspect_type': 'create', + 'callback_url': 'http://you.com/callback/', + 'created_at': '2015-04-29T18:11:09.400558047-07:00', + 'updated_at': '2015-04-29T18:11:09.400558047-07:00' }) - .then(result => { - result.should.eql({ - 'id': 1, - 'object_type': 'activity', - 'aspect_type': 'create', - 'callback_url': 'http://you.com/callback/', - 'created_at': '2015-04-29T18:11:09.400558047-07:00', - 'updated_at': '2015-04-29T18:11:09.400558047-07:00' - } - ) - }) }) }) describe('#delete({id:...})', function () { - before(() => { - // The status is not normally returned in the body. - // We return it here because the test can't easily access the HTTP status code. + it('should throw with no id', () => { + assert.throws(() => strava.pushSubscriptions.delete()) + }) + + it('should not sent Authorization header to Strava', async () => { nock('https://www.strava.com') - .filteringPath(() => '/api/v3/push_subscriptions/1/') - // The first reply just echo's the request headers so we can test them. - .delete(/^\/api\/v3\/push_subscriptions\/1/) + .delete('/api/v3/push_subscriptions/1') + .query(true) // Accept any query parameters + .once() .reply(200, function (uri, requestBody) { return { headers: this.req.headers } }) - .delete(/^\/api\/v3\/push_subscriptions\/1/) - .reply(204, function (uri, requestBody) { - requestBody = JSON.parse('{"status":204}') - return requestBody - }) - }) - it('should throw with no id', () => { - assert.throws(() => strava.pushSubscriptions.delete()) + const result = await strava.pushSubscriptions.delete({ id: 1 }) + assert.ok(!result.headers.authorization) }) - it('should not sent Authorization header to Strava', () => { - return strava.pushSubscriptions.delete({ id: 1 }) - .then(result => { - should.not.exist(result.headers.authorization) - }) - }) + it('Should return 204 after successful delete', async () => { + // The status is not normally returned in the body. + // We return it here because the test can't easily access the HTTP status code. + nock('https://www.strava.com') + .delete('/api/v3/push_subscriptions/1') + .query(true) // Accept any query parameters + .once() + .reply(204, { status: 204 }) - it('Should return 204 after successful delete', () => { - return strava.pushSubscriptions.delete({ id: 1 }) - .then(result => result.should.eql({ status: 204 })) + const result = await strava.pushSubscriptions.delete({ id: 1 }) + assert.deepStrictEqual(result, { status: 204 }) }) - - after(() => nock.restore()) }) }) diff --git a/test/rateLimiting.js b/test/rateLimiting.js index 98fa2a7..3f9ad3d 100644 --- a/test/rateLimiting.js +++ b/test/rateLimiting.js @@ -1,45 +1,45 @@ -var should = require('should') -var rateLimiting = require('../lib/rateLimiting') -var testHelper = require('./_helper') +const assert = require('assert') +const rateLimiting = require('../lib/rateLimiting') describe('rateLimiting_test', function () { describe('#fractionReached', function () { it('should update requestTime', function () { - var before = rateLimiting.requestTime - var headers = { + const headers = { 'date': 'Tue, 10 Oct 2013 20:11:05 GMT', 'x-ratelimit-limit': '600,30000', 'x-ratelimit-usage': '300,10000' } - rateLimiting.updateRateLimits(headers) + const result = rateLimiting.updateRateLimits(headers) - should(before).not.not.eql(rateLimiting.requestTime) + assert.ok(result) + assert.strictEqual(rateLimiting.shortTermUsage, 300) + assert.strictEqual(rateLimiting.shortTermLimit, 600) }) it('should calculate rate limit correctly', function () { - (rateLimiting.fractionReached()).should.eql(0.5) + assert.strictEqual(rateLimiting.fractionReached(), 0.5) }) it('should calculate rate limit correctly', function () { - var headers = { + const headers = { 'x-ratelimit-limit': '600,30000', 'x-ratelimit-usage': '300,27000' } - rateLimiting.updateRateLimits(headers); - (rateLimiting.fractionReached()).should.eql(0.9) + rateLimiting.updateRateLimits(headers) + assert.strictEqual(rateLimiting.fractionReached(), 0.9) }) it('should set values to zero when headers are nonsense', function () { - var headers = { + const headers = { 'x-ratelimit-limit': 'xxx', 'x-ratelimit-usage': 'zzz' } rateLimiting.updateRateLimits(headers) - rateLimiting.longTermUsage.should.eql(0) - rateLimiting.shortTermUsage.should.eql(0) - rateLimiting.longTermLimit.should.eql(0) - rateLimiting.shortTermLimit.should.eql(0) + assert.strictEqual(rateLimiting.longTermUsage, 0) + assert.strictEqual(rateLimiting.shortTermUsage, 0) + assert.strictEqual(rateLimiting.longTermLimit, 0) + assert.strictEqual(rateLimiting.shortTermLimit, 0) }) }) @@ -51,14 +51,14 @@ describe('rateLimiting_test', function () { rateLimiting.shortTermLimit = 100 rateLimiting.shortTermUsage = 200 - should(rateLimiting.exceeded()).be.true() + assert.strictEqual(rateLimiting.exceeded(), true) }) it('should not exceed rate limit when short usage is less than short term limit', function () { rateLimiting.shortTermLimit = 200 rateLimiting.shortTermUsage = 100 - rateLimiting.exceeded().should.be.false() + assert.strictEqual(rateLimiting.exceeded(), false) }) it('should exceed rate limit when long term usage exceeds limit', function () { @@ -67,14 +67,14 @@ describe('rateLimiting_test', function () { rateLimiting.longTermLimit = 100 rateLimiting.longTermUsage = 200 - rateLimiting.exceeded().should.be.true() + assert.strictEqual(rateLimiting.exceeded(), true) }) it('should not exceed rate limit when long term usage is less than long term limit', function () { rateLimiting.longTermLimit = 200 rateLimiting.longTermUsage = 100 - rateLimiting.exceeded().should.be.be.false() + assert.strictEqual(rateLimiting.exceeded(), false) }) }) }) diff --git a/test/routes.js b/test/routes.js index ecb672a..0539d5f 100644 --- a/test/routes.js +++ b/test/routes.js @@ -1,54 +1,148 @@ -var should = require('should') -var strava = require('../') -var testHelper = require('./_helper') +const assert = require('assert') +const strava = require('../') +const nock = require('nock') +const testHelper = require('./_helper') -var _sampleRoute +describe('routes', function () { + before(function () { + testHelper.setupMockAuth() + }) -describe('routes_test', function () { - before(function (done) { - testHelper.getSampleRoute(function (err, sampleRoute) { - if (err) { return done(err) } + afterEach(function () { + nock.cleanAll() + }) - _sampleRoute = sampleRoute - done() - }) + after(function () { + testHelper.cleanupAuth() }) describe('#get()', function () { - it('should return information about the corresponding route', function (done) { - strava.routes.get({ id: _sampleRoute.id }, function (err, payload) { - if (!err) { - should(payload.resource_state).be.exactly(3) - } else { - console.log(err) - } - - done() - }) + it('should return information about the corresponding route', async function () { + const routeId = '1234567890' + const mockRoute = { + id: routeId, + resource_state: 3, + name: 'Test Route', + description: 'A test route for testing', + athlete: { + id: 123456, + resource_state: 1 + }, + distance: 28099, + elevation_gain: 516, + map: { + id: 'r1234567890', + polyline: 'encoded_polyline_data', + resource_state: 3 + }, + type: 1, + sub_type: 1, + private: false, + starred: false, + timestamp: 1234567890, + created_at: '2023-01-01T00:00:00Z', + updated_at: '2023-01-01T00:00:00Z', + estimated_moving_time: 5400, + segments: [] + } + + nock('https://www.strava.com') + .get(`/api/v3/routes/${routeId}`) + .query(true) + .matchHeader('authorization', 'Bearer test_token') + .reply(200, mockRoute) + + const payload = await strava.routes.get({ id: routeId }) + + assert.strictEqual(payload.resource_state, 3) + assert.strictEqual(payload.id, routeId) + assert.strictEqual(payload.name, 'Test Route') + assert.strictEqual(typeof payload.distance, 'number') + assert.strictEqual(typeof payload.elevation_gain, 'number') + assert.ok(payload.map) + assert.strictEqual(payload.map.resource_state, 3) }) - it('should return the GPX file requested with the route information', function (done) { - strava.routes.getFile({ id: _sampleRoute.id, file_type: 'gpx' }, function (err, payload) { - if (!err) { - should(typeof payload).be.a.String() - } else { - console.log(err) - } + it('should return the GPX file requested with the route information', async function () { + const routeId = '1234567890' + const mockGpxData = ` + + + Test Route + A test route for testing + + + Test Route + + + 10.0 + + + 12.0 + + + +` + + nock('https://www.strava.com') + .get(`/api/v3/routes/${routeId}/export_gpx`) + .query(true) + .matchHeader('authorization', 'Bearer test_token') + .reply(200, mockGpxData, { + 'Content-Type': 'application/gpx+xml' + }) + + const payload = await strava.routes.getFile({ id: routeId, file_type: 'gpx' }) - done() - }) + assert.strictEqual(typeof payload, 'string') + assert.ok(payload.includes(' + + + + Test Route + + 5400 + 28099 + + 37.7749 + -122.4194 + + + + + + 37.7749 + -122.4194 + + 10.0 + + + + +` + + nock('https://www.strava.com') + .get(`/api/v3/routes/${routeId}/export_tcx`) + .query(true) + .matchHeader('authorization', 'Bearer test_token') + .reply(200, mockTcxData, { + 'Content-Type': 'application/tcx+xml' + }) + + const payload = await strava.routes.getFile({ id: routeId, file_type: 'tcx' }) - done() - }) + assert.strictEqual(typeof payload, 'string') + assert.ok(payload.includes('')) }) }) }) diff --git a/test/runningRaces.js b/test/runningRaces.js deleted file mode 100644 index 31f76ed..0000000 --- a/test/runningRaces.js +++ /dev/null @@ -1,28 +0,0 @@ -/* eslint handle-callback-err: 0 */ -var strava = require('../') -var testHelper = require('./_helper') - -var _sampleRunningRace - -describe('running_race_test', function () { - before(function (done) { - testHelper.getSampleRunningRace(function (err, sampleRunningRace) { - _sampleRunningRace = sampleRunningRace - done() - }) - }) - - describe('#get()', function () { - it('should return information about the corresponding race', function (done) { - strava.runningRaces.get({ id: _sampleRunningRace.id }, function (err, payload) { - if (!err) { - (payload.resource_state).should.be.exactly(3) - } else { - console.log(err) - } - - done() - }) - }) - }) -}) diff --git a/test/segmentEfforts.js b/test/segmentEfforts.js index 7b274f6..569f53d 100644 --- a/test/segmentEfforts.js +++ b/test/segmentEfforts.js @@ -1,29 +1,90 @@ +const assert = require('assert') +const strava = require('../') +const nock = require('nock') +const testHelper = require('./_helper') -var strava = require('../') -var testHelper = require('./_helper') +describe('segmentEfforts_test', function () { + before(function () { + testHelper.setupMockAuth() + }) -var _sampleSegmentEffort + afterEach(function () { + nock.cleanAll() + }) -describe.skip('segmentEfforts_test', function () { - before(function (done) { - // eslint-disable-next-line handle-callback-err - testHelper.getSampleSegmentEffort(function (err, payload) { - _sampleSegmentEffort = payload - done() - }) + after(function () { + testHelper.cleanupAuth() }) describe('#get()', function () { - it('should return detailed information about segment effort (level 3)', function (done) { - strava.segmentEfforts.get({ id: _sampleSegmentEffort.id }, function (err, payload) { - if (!err) { - (payload.resource_state).should.be.exactly(3) - } else { - console.log(err) + it('should return detailed information about segment effort (level 3)', async function () { + const segmentEffortId = 1234556789 + const mockResponse = { + id: 1234556789, + resource_state: 3, + name: "Alpe d'Huez", + activity: { + id: 3454504, + resource_state: 1 + }, + athlete: { + id: 54321, + resource_state: 1 + }, + elapsed_time: 381, + moving_time: 340, + start_date: '2018-02-12T16:12:41Z', + start_date_local: '2018-02-12T08:12:41Z', + distance: 83, + start_index: 65, + end_index: 83, + segment: { + id: 63450, + resource_state: 2, + name: "Alpe d'Huez", + activity_type: 'Run', + distance: 780.35, + average_grade: -0.5, + maximum_grade: 0, + elevation_high: 21, + elevation_low: 17.2, + start_latlng: [37.808407654682, -122.426682919323], + end_latlng: [37.808297909724, -122.421324329674], + climb_category: 0, + city: 'San Francisco', + state: 'CA', + country: 'United States', + private: false, + hazardous: false, + starred: false + }, + kom_rank: null, + pr_rank: null, + achievements: [], + athlete_segment_stats: { + pr_elapsed_time: 212, + pr_date: '2015-02-12', + effort_count: 149 } + } + + nock('https://www.strava.com') + .get(`/api/v3/segment_efforts/${segmentEffortId}`) + .matchHeader('authorization', 'Bearer test_token') + .reply(200, mockResponse) + + const payload = await strava.segmentEfforts.get({ id: segmentEffortId }) - done() - }) + assert.strictEqual(payload.id, 1234556789) + assert.strictEqual(payload.resource_state, 3) + assert.strictEqual(payload.name, "Alpe d'Huez") + assert.strictEqual(payload.elapsed_time, 381) + assert.strictEqual(payload.moving_time, 340) + assert.strictEqual(payload.distance, 83) + assert.strictEqual(payload.segment.id, 63450) + assert.strictEqual(payload.segment.name, "Alpe d'Huez") + assert.strictEqual(payload.segment.city, 'San Francisco') + assert.strictEqual(payload.athlete_segment_stats.effort_count, 149) }) }) }) diff --git a/test/segments.js b/test/segments.js index 4c195b2..8cf009e 100644 --- a/test/segments.js +++ b/test/segments.js @@ -1,123 +1,267 @@ +const assert = require('assert') +const strava = require('../') +const nock = require('nock') +const testHelper = require('./_helper') -var strava = require('../') -var testHelper = require('./_helper') - -var _sampleSegment - -describe('segments_test', function () { - before(function (done) { - testHelper.getSampleSegment(function (err, payload) { - if (err) { return done(err) } +describe('segments', function () { + beforeEach(function () { + nock.cleanAll() + testHelper.setupMockAuth() + }) - _sampleSegment = payload - done() - }) + afterEach(function () { + nock.cleanAll() + testHelper.cleanupAuth() }) describe('#get()', function () { - it('should return detailed information about segment (level 3)', function (done) { - strava.segments.get({ id: _sampleSegment.id }, function (err, payload) { - if (err) { return done(err) } - - if (!err) { - (payload.resource_state).should.be.exactly(3) - } else { - console.log(err) + it('should return detailed information about segment (level 3)', async function () { + const segmentId = '229781' + const mockSegment = { + id: 229781, + resource_state: 3, + name: 'Hawk Hill', + activity_type: 'Ride', + distance: 2684.82, + average_grade: 5.7, + maximum_grade: 14.2, + elevation_high: 245.3, + elevation_low: 92.4, + start_latlng: [37.8331119, -122.4834356], + end_latlng: [37.8280722, -122.4981393], + climb_category: 1, + city: 'San Francisco', + state: 'CA', + country: 'United States', + private: false, + hazardous: false, + starred: false, + created_at: '2009-09-21T20:29:41Z', + updated_at: '2018-02-15T09:04:18Z', + total_elevation_gain: 155.733, + map: { + id: 's229781', + resource_state: 3 + }, + effort_count: 309974, + athlete_count: 30623, + star_count: 2428, + athlete_segment_stats: { + pr_elapsed_time: 553, + pr_date: '1993-04-03', + effort_count: 2 } + } - done() - }) + nock('https://www.strava.com') + .get(`/api/v3/segments/${segmentId}`) + .query(true) + .matchHeader('authorization', 'Bearer test_token') + .once() + .reply(200, mockSegment) + + const payload = await strava.segments.get({ id: segmentId }) + + assert.strictEqual(payload.resource_state, 3) + assert.strictEqual(payload.id, 229781) + assert.strictEqual(payload.name, 'Hawk Hill') + assert.strictEqual(payload.activity_type, 'Ride') + assert.strictEqual(payload.distance, 2684.82) + assert.strictEqual(payload.city, 'San Francisco') }) }) describe('#listStarred()', function () { - it('should list segments currently starred by athlete', function (done) { - strava.segments.listStarred({ page: 1, per_page: 2 }, function (err, payload) { - if (!err) { - payload.should.be.instanceof(Array) - } else { - console.log(err) + it('should list segments currently starred by athlete', async function () { + const mockStarredSegments = [ + { + id: 229781, + resource_state: 3, + name: 'Hawk Hill', + activity_type: 'Ride', + distance: 2684.82, + average_grade: 5.7, + maximum_grade: 14.2, + elevation_high: 245.3, + elevation_low: 92.4, + start_latlng: [37.8331119, -122.4834356], + end_latlng: [37.8280722, -122.4981393], + climb_category: 1, + city: 'San Francisco', + state: 'CA', + country: 'United States', + private: false, + hazardous: false, + starred: true, + created_at: '2009-09-21T20:29:41Z', + updated_at: '2018-02-15T09:04:18Z', + total_elevation_gain: 155.733, + map: { + id: 's229781', + resource_state: 3 + }, + effort_count: 309974, + athlete_count: 30623, + star_count: 2428 } + ] - done() - }) + nock('https://www.strava.com') + .get('/api/v3/segments/starred') + .query({ page: 1, per_page: 2 }) + .matchHeader('authorization', 'Bearer test_token') + .once() + .reply(200, mockStarredSegments) + + const payload = await strava.segments.listStarred({ page: 1, per_page: 2 }) + + assert.ok(Array.isArray(payload)) + assert.ok(payload.length >= 1) + assert.strictEqual(payload[0].name, 'Hawk Hill') + assert.strictEqual(payload[0].starred, true) }) }) describe('#starSegment()', function () { - it('should toggle starred segment', function (done) { - var args = { id: _sampleSegment.id, starred: !_sampleSegment.starred } - strava.segments.starSegment(args, function (err, payload) { - if (!err) { - (payload.starred).should.be.exactly(!_sampleSegment.starred) - // revert segment star status back to original - args.starred = _sampleSegment.starred - strava.segments.starSegment(args, function (err, payload) { - if (!err) { - (payload.starred).should.be.exactly(_sampleSegment.starred) - } else { - console.log(err) - } - }) - } else { - console.log(err) - } + it('should toggle starred segment', async function () { + const segmentId = '229781' + const mockSegmentStarred = { + id: 229781, + resource_state: 3, + name: 'Hawk Hill', + starred: true, + activity_type: 'Ride' + } - done() - }) + nock('https://www.strava.com') + .put(`/api/v3/segments/${segmentId}/starred`) + .query(true) + .matchHeader('authorization', 'Bearer test_token') + .once() + .reply(200, mockSegmentStarred) + + const payload = await strava.segments.starSegment({ id: segmentId, starred: true }) + + assert.strictEqual(payload.starred, true) }) }) describe('#listEfforts()', function () { - it('should list efforts on segment by current athlete', function (done) { - strava.segments.listEfforts({ id: _sampleSegment.id, page: 1, per_page: 2 }, function (err, payload) { - if (!err) { - payload.should.be.instanceof(Array) - } else { - console.log(err) + it('should list efforts on segment by current athlete', async function () { + const segmentId = '229781' + const mockEfforts = [ + { + id: 1234567890, + resource_state: 2, + name: 'Effort 1', + activity: { id: 98765432, resource_state: 1 }, + athlete: { id: 123456, resource_state: 1 }, + elapsed_time: 600, + moving_time: 550, + start_date: '2018-02-15T09:00:00Z', + start_date_local: '2018-02-15T01:00:00Z' } + ] - done() - }) + nock('https://www.strava.com') + .get(`/api/v3/segments/${segmentId}/all_efforts`) + .query({ page: 1, per_page: 2 }) + .matchHeader('authorization', 'Bearer test_token') + .once() + .reply(200, mockEfforts) + + const payload = await strava.segments.listEfforts({ id: segmentId, page: 1, per_page: 2 }) + + assert.ok(Array.isArray(payload)) + assert.ok(payload.length >= 1) }) - it('should only provide efforts between dates if `start_date_local` & `end_date_local` parameters provided', function (done) { - var startDate = new Date(new Date() - 604800000) // last week - var endDate = new Date() - - var startString = startDate.toISOString() - var endString = endDate.toISOString() - - strava.segments.listEfforts({ id: _sampleSegment.id, page: 1, per_page: 10, start_date_local: startString, end_date_local: endString }, function (err, payload) { - if (!err) { - payload.forEach(function (item) { - var resultDate = new Date(item.start_date_local) - resultDate.should.be.greaterThan(startDate) - resultDate.should.be.lessThan(endDate) - }) - } else { - console.log(err) + it('should provide efforts within specified date range', async function () { + const segmentId = '229781' + const startDate = new Date(new Date() - 604800000) + const endDate = new Date() + + const startString = startDate.toISOString() + const endString = endDate.toISOString() + + const mockEffortsInRange = [ + { + id: 1234567890, + resource_state: 2, + name: 'Effort 1', + elapsed_time: 600, + moving_time: 550, + start_date_local: endDate.toISOString() } + ] + + nock('https://www.strava.com') + .get(`/api/v3/segments/${segmentId}/all_efforts`) + .query({ page: 1, per_page: 10, start_date_local: startString, end_date_local: endString }) + .matchHeader('authorization', 'Bearer test_token') + .once() + .reply(200, mockEffortsInRange) - done() + const payload = await strava.segments.listEfforts({ + id: segmentId, + page: 1, + per_page: 10, + start_date_local: startString, + end_date_local: endString + }) + + assert.ok(Array.isArray(payload)) + payload.forEach(effort => { + const resultDate = new Date(effort.start_date_local) + assert.ok(resultDate >= startDate) + assert.ok(resultDate <= endDate) }) }) }) describe('#explore()', function () { - it('should return up to 10 segments w/i the given bounds', function (done) { - strava.segments.explore({ + it('should return segments within given bounds', async function () { + const mockExploreResponse = { + segments: [ + { + id: 229781, + resource_state: 2, + name: 'Hawk Hill', + activity_type: 'Ride', + distance: 2684.82, + average_grade: 5.7, + maximum_grade: 14.2, + elevation_high: 245.3, + elevation_low: 92.4, + start_latlng: [37.8331119, -122.4834356], + end_latlng: [37.8280722, -122.4981393], + climb_category: 1, + city: 'San Francisco', + state: 'CA', + country: 'United States', + private: false, + hazardous: false, + starred: false + } + ] + } + + nock('https://www.strava.com') + .get('/api/v3/segments/explore') + .query({ bounds: '37.821362,-122.505373,37.842038,-122.465977', activity_type: 'running' }) + .matchHeader('authorization', 'Bearer test_token') + .once() + .reply(200, mockExploreResponse) + + const payload = await strava.segments.explore({ bounds: '37.821362,-122.505373,37.842038,-122.465977', activity_type: 'running' - }, function (err, payload) { - if (!err) { - payload.segments.should.be.instanceof(Array) - } else { - console.log(err) - } - - done() }) + + assert.ok(payload.segments) + assert.ok(Array.isArray(payload.segments)) + assert.ok(payload.segments.length >= 1) + assert.strictEqual(payload.segments[0].name, 'Hawk Hill') }) }) }) diff --git a/test/streams.js b/test/streams.js index d8da7ac..202dd1f 100644 --- a/test/streams.js +++ b/test/streams.js @@ -1,106 +1,189 @@ /* eslint camelcase: 0 */ -var strava = require('../') -var testHelper = require('./_helper') - -var _activity_id = '2725479568' -var _segmentEffort_id = '68090153244' -var _segment_id = '68090153244' -var _route_id = '' - -var _sampleActivity - -describe('streams_test', function () { - before(function (done) { - this.timeout(5000) - - testHelper.getSampleActivity(function (err, payload) { - if (err) { return done(err) } - - _sampleActivity = payload - - _activity_id = _sampleActivity.id - // _segmentEffort_id = _sampleActivity.segment_efforts[0].id; - // _segment_id = _sampleActivity.segment_efforts[0].segment.id; - - testHelper.getSampleRoute(function (err, payload) { - _route_id = payload && payload.id +const assert = require('assert') +const strava = require('../') +const nock = require('nock') +const testHelper = require('./_helper') + +describe('streams', function () { + beforeEach(function () { + // Clean all nock interceptors before each test to ensure isolation + nock.cleanAll() + testHelper.setupMockAuth() + }) - done(err) - }) - }) + afterEach(function () { + nock.cleanAll() + testHelper.cleanupAuth() }) describe('#activity()', function () { - it('should return raw data associated to activity', function (done) { - strava.streams.activity({ - id: _activity_id, - types: 'time,distance', - resolution: 'low' - }, function (err, payload) { - if (!err) { - payload.should.be.instanceof(Array) - } else { - console.log(err) + it('should return raw data associated to activity', async function () { + const activityId = '2725479568' + const mockStreams = [ + { + type: 'distance', + data: [2.9, 5.8, 8.5, 11.7, 15, 19, 23.2, 28, 32.8, 38.1, 43.8, 49.5], + series_type: 'distance', + original_size: 12, + resolution: 'high' } - - done() + ] + + nock('https://www.strava.com') + .get(`/api/v3/activities/${activityId}/streams`) + .query({ + keys: 'time,distance', + resolution: 'low' + }) + .matchHeader('authorization', 'Bearer test_token') + .reply(200, mockStreams) + + const payload = await strava.streams.activity({ + id: activityId, + keys: 'time,distance', + resolution: 'low' }) + + assert.ok(Array.isArray(payload)) + assert.ok(payload.length >= 1) + // Check that we have distance stream + const distanceStream = payload.find(stream => stream.type === 'distance') + assert.ok(distanceStream) + assert.ok(Array.isArray(distanceStream.data)) + assert.ok(distanceStream.data.length > 0) }) }) - describe.skip('#effort()', function () { - it('should return raw data associated to segment_effort', function (done) { - strava.streams.effort({ - id: _segmentEffort_id, - types: 'distance', - resolution: 'low' - }, function (err, payload) { - if (!err) { - payload.should.be.instanceof(Array) - } else { - console.log(err) + describe('#effort()', function () { + it('should return raw data associated to segment_effort', async function () { + const segmentEffortId = '68090153244' + const mockStreams = [ + { + type: 'distance', + data: [904.5, 957.8, 963.1, 989.1, 1011.9, 1049.7, 1082.4, 1098.1, 1113.2, 1124.7, 1139.2, 1142.1, 1170.4, 1173], + series_type: 'distance', + original_size: 14, + resolution: 'high' } - - done() + ] + + nock('https://www.strava.com') + .get(`/api/v3/segment_efforts/${segmentEffortId}/streams`) + .query({ + keys: 'distance', + resolution: 'low' + }) + .matchHeader('authorization', 'Bearer test_token') + .reply(200, mockStreams) + + const payload = await strava.streams.effort({ + id: segmentEffortId, + keys: 'distance', + resolution: 'low' }) + + assert.ok(Array.isArray(payload)) + assert.strictEqual(payload.length, 1) + assert.strictEqual(payload[0].type, 'distance') + assert.ok(Array.isArray(payload[0].data)) + assert.ok(payload[0].data.length > 0) }) }) - describe.skip('#segment()', function () { - it('should return raw data associated to segment', function (done) { - strava.streams.segment({ - id: _segment_id, - types: 'distance', - resolution: 'low' - }, function (err, payload) { - if (!err) { - payload.should.be.instanceof(Array) - } else { - console.log(err) + describe('#segment()', function () { + it('should return raw data associated to segment', async function () { + const segmentId = '646257' + const mockStreams = [ + { + type: 'latlng', + data: [[37.833112, -122.483436], [37.832964, -122.483406]], + series_type: 'distance', + original_size: 2, + resolution: 'high' + }, + { + type: 'distance', + data: [0, 16.8], + series_type: 'distance', + original_size: 2, + resolution: 'high' + }, + { + type: 'altitude', + data: [92.4, 93.4], + series_type: 'distance', + original_size: 2, + resolution: 'high' } + ] + + nock('https://www.strava.com') + .get(`/api/v3/segments/${segmentId}/streams`) + .query({ + keys: 'distance', + resolution: 'low' + }) + .matchHeader('authorization', 'Bearer test_token') + .reply(200, mockStreams) + + const payload = await strava.streams.segment({ + id: segmentId, + keys: 'distance', + resolution: 'low' + }) - done() + assert.ok(Array.isArray(payload)) + assert.ok(payload.length >= 1) + // Verify we have expected stream types + const streamTypes = payload.map(stream => stream.type) + assert.ok(streamTypes.includes('distance') || streamTypes.includes('latlng') || streamTypes.includes('altitude')) + // Check all streams have data + payload.forEach(stream => { + assert.ok(Array.isArray(stream.data)) + assert.ok(stream.data.length > 0) }) }) }) describe('#route()', function () { - this.timeout(5000) + it('should return raw data associated to route', async function () { + const routeId = '12345678' + const mockStreams = [ + { + type: 'latlng', + data: [[37.833112, -122.483436], [37.832964, -122.483406]] + }, + { + type: 'distance', + data: [0, 16.8] + }, + { + type: 'altitude', + data: [92.4, 93.4] + } + ] - it('should return raw data associated to route', function (done) { - strava.streams.route({ - id: _route_id, - types: '', + nock('https://www.strava.com') + .get(`/api/v3/routes/${routeId}/streams`) + .query(true) // Accept any query parameters since types is empty + .matchHeader('authorization', 'Bearer test_token') + .reply(200, mockStreams) + + const payload = await strava.streams.route({ + id: routeId, resolution: 'low' - }, function (err, payload) { - if (!err) { - payload.should.be.instanceof(Array) - } else { - console.log(err) - } + }) - done() + assert.ok(Array.isArray(payload)) + assert.ok(payload.length >= 1) + // Verify we have the expected stream types + const streamTypes = payload.map(stream => stream.type) + assert.ok(streamTypes.includes('latlng') || streamTypes.includes('distance') || streamTypes.includes('altitude')) + // Check that all streams have data arrays + payload.forEach(stream => { + assert.ok(Array.isArray(stream.data)) + assert.ok(stream.data.length > 0) }) }) }) diff --git a/test/uploads.js b/test/uploads.js index 25958c0..cb7ac70 100644 --- a/test/uploads.js +++ b/test/uploads.js @@ -1,38 +1,134 @@ -require('es6-promise').polyfill() - -var should = require('should') +const assert = require('assert') const strava = require('../') +const nock = require('nock') +const testHelper = require('./_helper') +const tmp = require('tmp') +const fs = require('fs') describe('uploads_test', function () { + beforeEach(function () { + // Clean all nock interceptors before each test to ensure isolation + nock.cleanAll() + testHelper.setupMockAuth() + }) + + afterEach(function () { + nock.cleanAll() + testHelper.cleanupAuth() + }) + describe('#post()', function () { it('should upload a GPX file', async () => { - // Update datetime in gpx file to now, so Strava doesn't reject it as a duplicate - const sampleGpxFilename = 'test/assets/gpx_sample.gpx' - const gpxFilename = 'data/gpx_temp.gpx' - const now = new Date() - let gpx = require('fs').readFileSync(sampleGpxFilename, 'utf8') - gpx = gpx.replaceAll(/