74 |
75 | Loading notifications... |
76 |
├── lib ├── plugins │ ├── basecamp.js │ ├── google.js │ ├── jira.js │ ├── trello.js │ ├── teuxdeux.js │ ├── pivotal.js │ ├── github.js │ └── sprintly.js ├── scsf.js └── tuddy.js ├── dist ├── plugins │ ├── basecamp.js │ ├── google.js │ ├── jira.js │ ├── trello.js │ ├── teuxdeux.js │ ├── pivotal.js │ ├── github.js │ └── sprintly.js ├── scsf.js └── tuddy.js ├── .gitignore ├── gulpfile.js ├── fixtures ├── sprint.ly-443 │ ├── 143956727788521884 │ ├── 143956743018262337 │ ├── 143956781026318453 │ ├── 143956904868389120 │ ├── 14440476357067901 │ ├── 144404746877869619 │ ├── 144404746878262524 │ ├── 144404763570338337 │ ├── 144404769950086401 │ ├── 144404769953725668 │ ├── 14440479455932645 │ ├── 14440479456231649 │ ├── 144404896070044788 │ ├── 144404896075932098 │ ├── 14440489702863834 │ ├── 144404897029563543 │ ├── 144404897663516032 │ ├── 144404897665374750 │ ├── 144404903034771246 │ └── 144404903039163197 ├── teuxdeux.com-443 │ ├── 144344327345316527 │ ├── 144344327392491973 │ ├── 14434432736474317 │ ├── 144344327365530891 │ ├── 144344327408858641 │ └── 144344327282465992 ├── api.github.com-443 │ ├── 144198517846056797 │ ├── 144198517846155868 │ ├── 143951901414911143 │ ├── 143951901415142161 │ ├── 144198978717276008 │ ├── 144198978717419136 │ ├── 144404589778783142 │ ├── 144404589779179205 │ ├── 143948298586250163 │ ├── 14394829859068991 │ └── 143948298609163410 ├── www.pivotaltracker.com-443 │ ├── 143951561568642871 │ ├── 143950789799014555 │ ├── 143950789818378886 │ ├── 143950726207840389 │ ├── 143950726220617327 │ └── 1439515615240831 ├── tudddy.atlassian.net-443 │ ├── 143950153298946057 │ ├── 143950153299281116 │ └── 144199461069779751 └── api.trello.com-443 │ ├── 143948376866963411 │ ├── 143948377010915063 │ ├── 144380891553252080 │ ├── 144380891561814173 │ ├── 143948361720759024 │ └── 143948377065823795 ├── package.json ├── test ├── teuxdeux.spec.js ├── index.js ├── google.spec.js ├── sprintly.spec.js ├── pivotal.spec.js ├── trello.spec.js ├── github.spec.js └── jira.spec.js └── README.md /lib/plugins/basecamp.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/plugins/google.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/plugins/basecamp.js: -------------------------------------------------------------------------------- 1 | "use strict"; -------------------------------------------------------------------------------- /dist/plugins/google.js: -------------------------------------------------------------------------------- 1 | "use strict"; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | 4 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require("gulp"); 2 | var babel = require("gulp-babel"); 3 | 4 | gulp.task("default", function () { 5 | return gulp.src("lib/**") 6 | .pipe(babel()) 7 | .pipe(gulp.dest("dist")); 8 | }); 9 | -------------------------------------------------------------------------------- /fixtures/sprint.ly-443/143956727788521884: -------------------------------------------------------------------------------- 1 | POST /api/products/33921/items.json 2 | authorization: Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG 3 | host: sprint.ly 4 | accept: application/json 5 | content-type: application/json 6 | body: {\"title\":\"test4\",\"type\":\"story\"} 7 | 8 | HTTP/1.1 400 undefined 9 | content-type: application/json 10 | date: Fri, 14 Aug 2015 15:47:57 GMT 11 | server: nginx/1.1.19 12 | x-frame-options: SAMEORIGIN 13 | content-length: 53 14 | connection: keep-alive 15 | 16 | {"message":"type is a required argument.","code":400} -------------------------------------------------------------------------------- /fixtures/sprint.ly-443/143956743018262337: -------------------------------------------------------------------------------- 1 | POST /api/products/33921/items.json 2 | authorization: Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG 3 | host: sprint.ly 4 | accept: application/json 5 | content-type: application/json 6 | body: {\"title\":\"test4\",\"type\":\"task\"} 7 | 8 | HTTP/1.1 400 undefined 9 | content-type: application/json 10 | date: Fri, 14 Aug 2015 15:50:30 GMT 11 | server: nginx/1.1.19 12 | x-frame-options: SAMEORIGIN 13 | content-length: 53 14 | connection: keep-alive 15 | 16 | {"message":"type is a required argument.","code":400} -------------------------------------------------------------------------------- /fixtures/sprint.ly-443/143956781026318453: -------------------------------------------------------------------------------- 1 | POST /api/products/33921/items.json 2 | authorization: Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG 3 | host: sprint.ly 4 | accept: application/json 5 | content-type: application/json 6 | body: {\"title\":\"test3\",\"description\":\"hello\",\"type\":\"task\"} 7 | 8 | HTTP/1.1 400 undefined 9 | content-type: application/json 10 | date: Fri, 14 Aug 2015 15:56:50 GMT 11 | server: nginx/1.1.19 12 | x-frame-options: SAMEORIGIN 13 | content-length: 53 14 | connection: keep-alive 15 | 16 | {"message":"type is a required argument.","code":400} -------------------------------------------------------------------------------- /fixtures/teuxdeux.com-443/144344327345316527: -------------------------------------------------------------------------------- 1 | POST /login 2 | host: teuxdeux.com 3 | content-type: application/x-www-form-urlencoded 4 | body: username=tuddy&password=tuddypass&authenticity_token=d151f2a1c31d5b5374f64112be07c3cd637b9f8b6450fa54ee3dd492948eed0c 5 | 6 | HTTP/1.1 302 Found 7 | content-type: text/html;charset=utf-8 8 | date: Mon, 28 Sep 2015 12:27:53 GMT 9 | location: https://teuxdeux.com/ 10 | server: nginx/1.2.1 11 | set-cookie: rack.session=BAh7CUkiD3Nlc3Npb25faWQGOgZFRiJFNTA4YWZiZDdlZTdjOTViOWQ2YThi%0AZDg0ZDI5ODI1MmFlYThmNzMwNjcwMjdmMThiMWRlYTgyYTg1NDI5ZmNlNEki%0ADl9fRkxBU0hfXwY7AEZ7AEkiCWNzcmYGOwBGIkVkMTUxZjJhMWMzMWQ1YjUz%0ANzRmNjQxMTJiZTA3YzNjZDYzN2I5ZjhiNjQ1MGZhNTRlZTNkZDQ5Mjk0OGVl%0AZDBjSSIMdXNlcl9pZAY7AEZpAizW%0A--fae485c6811e7ef3d7c07d25450f0f0e814f5570; path=/; expires=Wed, 28 Oct 2015 12:27:53 -0000; secure; HttpOnly 12 | status: 302 Found 13 | x-frame-options: sameorigin, SAMEORIGIN 14 | x-xss-protection: 1; mode=block 15 | content-length: 0 16 | connection: Close 17 | 18 | -------------------------------------------------------------------------------- /fixtures/teuxdeux.com-443/144344327392491973: -------------------------------------------------------------------------------- 1 | POST /login 2 | host: teuxdeux.com 3 | content-type: application/x-www-form-urlencoded 4 | body: username=tuddy&password=tuddypass&authenticity_token=d151f2a1c31d5b5374f64112be07c3cd637b9f8b6450fa54ee3dd492948eed0c 5 | 6 | HTTP/1.1 302 Found 7 | content-type: text/html;charset=utf-8 8 | date: Mon, 28 Sep 2015 12:27:54 GMT 9 | location: https://teuxdeux.com/ 10 | server: nginx/1.2.1 11 | set-cookie: rack.session=BAh7CUkiD3Nlc3Npb25faWQGOgZFRiJFNTA4YWZiZDdlZTdjOTViOWQ2YThi%0AZDg0ZDI5ODI1MmFlYThmNzMwNjcwMjdmMThiMWRlYTgyYTg1NDI5ZmNlNEki%0ADl9fRkxBU0hfXwY7AEZ7AEkiCWNzcmYGOwBGIkVkMTUxZjJhMWMzMWQ1YjUz%0ANzRmNjQxMTJiZTA3YzNjZDYzN2I5ZjhiNjQ1MGZhNTRlZTNkZDQ5Mjk0OGVl%0AZDBjSSIMdXNlcl9pZAY7AEZpAizW%0A--fae485c6811e7ef3d7c07d25450f0f0e814f5570; path=/; expires=Wed, 28 Oct 2015 12:27:54 -0000; secure; HttpOnly 12 | status: 302 Found 13 | x-frame-options: sameorigin, SAMEORIGIN 14 | x-xss-protection: 1; mode=block 15 | content-length: 0 16 | connection: Close 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tuddy", 3 | "version": "1.1.8", 4 | "description": "Import, export, synchronize Pivotal Tracker, GitHub Issues, Trello, JIRA, Sprintly, Teuxdeux, and all Super Common Story Format compatible mapped systems", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/specerator/tuddy" 8 | }, 9 | "homepage": "http://specerator.com", 10 | "main": "dist/tuddy.js", 11 | "scripts": { 12 | "test": "REPLAY=record mocha --compilers js:babel/register test/" 13 | }, 14 | "author": "Jeff Loiselle", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "babel": "^5.8.21", 18 | "chai": "^3.2.0", 19 | "gulp": "^3.9.0", 20 | "gulp-babel": "^5.2.1", 21 | "mocha": "^2.2.5", 22 | "replay": "^2.0.6", 23 | "sinon": "^1.15.4" 24 | }, 25 | "dependencies": { 26 | "async": "^1.4.2", 27 | "bluebird": "^2.9.34", 28 | "cheerio": "^0.19.0", 29 | "googleapis": "^2.1.3", 30 | "jira-connector": "^1.4.1", 31 | "lodash": "^3.10.1", 32 | "node-trello": "^1.1.2", 33 | "request": "^2.60.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/teuxdeux.spec.js: -------------------------------------------------------------------------------- 1 | require('replay'); 2 | var expect = require('chai').expect; 3 | 4 | var Teuxdeux = require('../lib/plugins/teuxdeux.js'); 5 | 6 | describe('Teuxdeux', () => { 7 | describe('#push', () => { 8 | it('should be able to push stories', (done) => { 9 | let integration = { username: 'tuddy', password: 'tuddypass' }; 10 | var teuxdeux = new Teuxdeux(integration); 11 | let stories = [ 12 | { 13 | name: 'test3', 14 | date: { 15 | start: '2015-09-11' 16 | } 17 | }, 18 | { 19 | name: 'test4', 20 | date: { 21 | start: '2015-09-11' 22 | } 23 | } 24 | ]; 25 | teuxdeux.push(stories).then((stories) => { 26 | expect(stories).to.have.length(2); 27 | done(); 28 | }); 29 | }); 30 | }); 31 | describe('#pull', () => { 32 | it('should be able to pull stories', (done) => { 33 | let integration = { username: 'tuddy', password: 'tuddypass' }; 34 | var teuxdeux = new Teuxdeux(integration); 35 | teuxdeux.pull().then((stories) => { 36 | expect(stories).to.have.length(6); 37 | done(); 38 | }); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /fixtures/api.github.com-443/144198517846056797: -------------------------------------------------------------------------------- 1 | POST /repos/tudddy/tuddy/issues?access_token=c136002525ae2a82dec9109c304167226f8f7e7e 2 | content-type: application/json 3 | host: api.github.com 4 | accept: application/json 5 | body: {\"name\":\"test4\",\"description\":null,\"story_type\":\"feature\"} 6 | 7 | HTTP/1.1 401 undefined 8 | server: GitHub.com 9 | date: Fri, 11 Sep 2015 15:26:18 GMT 10 | content-type: application/json; charset=utf-8 11 | content-length: 83 12 | status: 401 Unauthorized 13 | x-github-media-type: github.v3 14 | x-ratelimit-limit: 60 15 | x-ratelimit-remaining: 58 16 | x-ratelimit-reset: 1441988778 17 | x-xss-protection: 1; mode=block 18 | x-frame-options: deny 19 | content-security-policy: default-src 'none' 20 | access-control-allow-credentials: true 21 | access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 22 | access-control-allow-origin: * 23 | x-github-request-id: 183D2EF0:19DA:BE3FA31:55F2F29A 24 | strict-transport-security: max-age=31536000; includeSubdomains; preload 25 | x-content-type-options: nosniff 26 | 27 | {"message":"Bad credentials","documentation_url":"https://developer.github.com/v3"} -------------------------------------------------------------------------------- /fixtures/api.github.com-443/144198517846155868: -------------------------------------------------------------------------------- 1 | POST /repos/tudddy/tuddy/issues?access_token=c136002525ae2a82dec9109c304167226f8f7e7e 2 | content-type: application/json 3 | host: api.github.com 4 | accept: application/json 5 | body: {\"name\":\"test3\",\"description\":null,\"story_type\":\"feature\"} 6 | 7 | HTTP/1.1 401 undefined 8 | server: GitHub.com 9 | date: Fri, 11 Sep 2015 15:26:18 GMT 10 | content-type: application/json; charset=utf-8 11 | content-length: 83 12 | status: 401 Unauthorized 13 | x-github-media-type: github.v3 14 | x-ratelimit-limit: 60 15 | x-ratelimit-remaining: 59 16 | x-ratelimit-reset: 1441988778 17 | x-xss-protection: 1; mode=block 18 | x-frame-options: deny 19 | content-security-policy: default-src 'none' 20 | access-control-allow-credentials: true 21 | access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 22 | access-control-allow-origin: * 23 | x-github-request-id: 183D2EF0:19DA:BE3FA34:55F2F29A 24 | strict-transport-security: max-age=31536000; includeSubdomains; preload 25 | x-content-type-options: nosniff 26 | 27 | {"message":"Bad credentials","documentation_url":"https://developer.github.com/v3"} -------------------------------------------------------------------------------- /lib/scsf.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | class SCSF { 4 | static merge(data) { 5 | var schema = SCSF.schema(); 6 | return _.merge(schema, data); 7 | } 8 | static schema() { 9 | return { 10 | meta: { 11 | source: { 12 | id: null, 13 | name: null, 14 | key: null, 15 | data: null 16 | } 17 | }, 18 | data: { 19 | id: null, 20 | self: null, 21 | key: null, 22 | name: null, 23 | description: null, 24 | type: null, 25 | url: null, 26 | archived: null, 27 | status: null, 28 | email: null, 29 | short_url: null, 30 | date: { 31 | start: null, 32 | end: null, 33 | due: null, 34 | created: null, 35 | updated: null, 36 | completed: null, 37 | deleted: null 38 | }, 39 | lists: [ 40 | 41 | ], 42 | labels: [ 43 | 44 | ], 45 | project: { 46 | id: null 47 | }, 48 | document: { 49 | id: null, 50 | range: null 51 | }, 52 | users: [ 53 | 54 | ] 55 | } 56 | } 57 | } 58 | } 59 | 60 | module.exports = SCSF; 61 | -------------------------------------------------------------------------------- /fixtures/www.pivotaltracker.com-443/143951561568642871: -------------------------------------------------------------------------------- 1 | GET /services/v5/projects/1407870/stories?offset=100 2 | x-trackertoken: 690eb36760eff666af11c563510fc616 3 | content-type: application/json 4 | host: www.pivotaltracker.com 5 | accept: application/json 6 | 7 | HTTP/1.1 200 undefined 8 | content-type: application/json; charset=utf-8 9 | transfer-encoding: chunked 10 | status: 200 OK 11 | x-tracker-project-version: 8 12 | x-tracker-pagination-total: 7 13 | x-tracker-pagination-offset: 100 14 | x-tracker-pagination-limit: 100 15 | x-tracker-pagination-returned: 0 16 | x-ua-compatible: IE=Edge,chrome=1 17 | etag: "d751713988987e9331980363e24189ce" 18 | cache-control: max-age=0, private, must-revalidate 19 | x-request-id: 9c5bf1250c8ab3c0d09f6bf9ec117e15 20 | x-runtime: 0.050022 21 | date: Fri, 14 Aug 2015 01:26:55 GMT 22 | x-rack-cache: miss 23 | x-powered-by: Phusion Passenger 4.0.41 24 | server: nginx/1.6.0 + Phusion Passenger 4.0.41 25 | access-control-allow-origin: * 26 | access-control-allow-credentials: false 27 | access-control-allow-methods: GET, POST, PUT, DELETE, OPTIONS 28 | access-control-allow-headers: X-TrackerToken,DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,X-Tracker-Warn-Unless-Project-Version-Is 29 | x-tracker-client-pinger-interval: 12 30 | 31 | [] -------------------------------------------------------------------------------- /fixtures/api.github.com-443/143951901414911143: -------------------------------------------------------------------------------- 1 | POST /repos/tudddy/tuddy/issues?access_token=c136002525ae2a82dec9109c304167226f8f7e7e 2 | content-type: application/json 3 | host: api.github.com 4 | accept: application/json 5 | body: {\"name\":\"test4\",\"story_type\":\"feature\"} 6 | 7 | HTTP/1.1 422 undefined 8 | server: GitHub.com 9 | date: Fri, 14 Aug 2015 02:23:34 GMT 10 | content-type: application/json; charset=utf-8 11 | content-length: 138 12 | status: 422 Unprocessable Entity 13 | x-ratelimit-limit: 5000 14 | x-ratelimit-remaining: 4998 15 | x-ratelimit-reset: 1439522614 16 | x-oauth-scopes: gist, repo, user 17 | x-accepted-oauth-scopes: 18 | x-github-media-type: github.v3 19 | x-xss-protection: 1; mode=block 20 | x-frame-options: deny 21 | content-security-policy: default-src 'none' 22 | access-control-allow-credentials: true 23 | access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 24 | access-control-allow-origin: * 25 | x-github-request-id: 183D2EF0:3D4C:2D09023:55CD5126 26 | strict-transport-security: max-age=31536000; includeSubdomains; preload 27 | x-content-type-options: nosniff 28 | 29 | {"message":"Invalid request.\n\n\"title\" wasn't supplied.","documentation_url":"https://developer.github.com/v3/issues/#create-an-issue"} -------------------------------------------------------------------------------- /fixtures/api.github.com-443/143951901415142161: -------------------------------------------------------------------------------- 1 | POST /repos/tudddy/tuddy/issues?access_token=c136002525ae2a82dec9109c304167226f8f7e7e 2 | content-type: application/json 3 | host: api.github.com 4 | accept: application/json 5 | body: {\"name\":\"test3\",\"story_type\":\"feature\"} 6 | 7 | HTTP/1.1 422 undefined 8 | server: GitHub.com 9 | date: Fri, 14 Aug 2015 02:23:34 GMT 10 | content-type: application/json; charset=utf-8 11 | content-length: 138 12 | status: 422 Unprocessable Entity 13 | x-ratelimit-limit: 5000 14 | x-ratelimit-remaining: 4999 15 | x-ratelimit-reset: 1439522614 16 | x-oauth-scopes: gist, repo, user 17 | x-accepted-oauth-scopes: 18 | x-github-media-type: github.v3 19 | x-xss-protection: 1; mode=block 20 | x-frame-options: deny 21 | content-security-policy: default-src 'none' 22 | access-control-allow-credentials: true 23 | access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 24 | access-control-allow-origin: * 25 | x-github-request-id: 183D2EF0:3D42:227E3FB:55CD5126 26 | strict-transport-security: max-age=31536000; includeSubdomains; preload 27 | x-content-type-options: nosniff 28 | 29 | {"message":"Invalid request.\n\n\"title\" wasn't supplied.","documentation_url":"https://developer.github.com/v3/issues/#create-an-issue"} -------------------------------------------------------------------------------- /fixtures/api.github.com-443/144198978717276008: -------------------------------------------------------------------------------- 1 | POST /repos/tudddy/tuddy/issues?access_token=c136002525ae2a82dec9109c304167226f8f7e7e 2 | content-type: application/json 3 | host: api.github.com 4 | accept: application/json 5 | body: {\"id\":null,\"name\":\"test3\",\"body\":null,\"story_type\":\"feature\",\"url\":null,\"html_url\":null,\"created_at\":null,\"updated_at\":null,\"closed_at\":null,\"number\":null,\"state\":\"closed\"} 6 | 7 | HTTP/1.1 401 undefined 8 | server: GitHub.com 9 | date: Fri, 11 Sep 2015 16:43:07 GMT 10 | content-type: application/json; charset=utf-8 11 | content-length: 83 12 | status: 401 Unauthorized 13 | x-github-media-type: github.v3 14 | x-ratelimit-limit: 60 15 | x-ratelimit-remaining: 58 16 | x-ratelimit-reset: 1441992646 17 | x-xss-protection: 1; mode=block 18 | x-frame-options: deny 19 | content-security-policy: default-src 'none' 20 | access-control-allow-credentials: true 21 | access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 22 | access-control-allow-origin: * 23 | x-github-request-id: 183D2EF0:1A01:C2DC32D:55F3049B 24 | strict-transport-security: max-age=31536000; includeSubdomains; preload 25 | x-content-type-options: nosniff 26 | 27 | {"message":"Bad credentials","documentation_url":"https://developer.github.com/v3"} -------------------------------------------------------------------------------- /fixtures/api.github.com-443/144198978717419136: -------------------------------------------------------------------------------- 1 | POST /repos/tudddy/tuddy/issues?access_token=c136002525ae2a82dec9109c304167226f8f7e7e 2 | content-type: application/json 3 | host: api.github.com 4 | accept: application/json 5 | body: {\"id\":null,\"name\":\"test4\",\"body\":null,\"story_type\":\"feature\",\"url\":null,\"html_url\":null,\"created_at\":null,\"updated_at\":null,\"closed_at\":null,\"number\":null,\"state\":\"closed\"} 6 | 7 | HTTP/1.1 401 undefined 8 | server: GitHub.com 9 | date: Fri, 11 Sep 2015 16:43:07 GMT 10 | content-type: application/json; charset=utf-8 11 | content-length: 83 12 | status: 401 Unauthorized 13 | x-github-media-type: github.v3 14 | x-ratelimit-limit: 60 15 | x-ratelimit-remaining: 57 16 | x-ratelimit-reset: 1441992646 17 | x-xss-protection: 1; mode=block 18 | x-frame-options: deny 19 | content-security-policy: default-src 'none' 20 | access-control-allow-credentials: true 21 | access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 22 | access-control-allow-origin: * 23 | x-github-request-id: 183D2EF0:1A00:98FE8BC:55F3049B 24 | strict-transport-security: max-age=31536000; includeSubdomains; preload 25 | x-content-type-options: nosniff 26 | 27 | {"message":"Bad credentials","documentation_url":"https://developer.github.com/v3"} -------------------------------------------------------------------------------- /fixtures/api.github.com-443/144404589778783142: -------------------------------------------------------------------------------- 1 | POST /repos/tudddy/tuddy/issues?access_token=c136002525ae2a82dec9109c304167226f8f7e7e 2 | content-type: application/json 3 | host: api.github.com 4 | accept: application/json 5 | body: {\"id\":null,\"title\":\"test3\",\"body\":null,\"story_type\":\"feature\",\"url\":null,\"html_url\":null,\"created_at\":null,\"updated_at\":null,\"closed_at\":null,\"number\":null,\"state\":\"closed\"} 6 | 7 | HTTP/1.1 401 Unauthorized 8 | server: GitHub.com 9 | date: Mon, 05 Oct 2015 11:51:37 GMT 10 | content-type: application/json; charset=utf-8 11 | content-length: 83 12 | connection: close 13 | status: 401 Unauthorized 14 | x-github-media-type: github.v3 15 | x-ratelimit-limit: 60 16 | x-ratelimit-remaining: 59 17 | x-ratelimit-reset: 1444049497 18 | x-xss-protection: 1; mode=block 19 | x-frame-options: deny 20 | content-security-policy: default-src 'none' 21 | access-control-allow-credentials: true 22 | access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 23 | access-control-allow-origin: * 24 | x-github-request-id: 183D2EF0:17F57:77F756F:56126449 25 | strict-transport-security: max-age=31536000; includeSubdomains; preload 26 | x-content-type-options: nosniff 27 | 28 | {"message":"Bad credentials","documentation_url":"https://developer.github.com/v3"} -------------------------------------------------------------------------------- /fixtures/api.github.com-443/144404589779179205: -------------------------------------------------------------------------------- 1 | POST /repos/tudddy/tuddy/issues?access_token=c136002525ae2a82dec9109c304167226f8f7e7e 2 | content-type: application/json 3 | host: api.github.com 4 | accept: application/json 5 | body: {\"id\":null,\"title\":\"test4\",\"body\":null,\"story_type\":\"feature\",\"url\":null,\"html_url\":null,\"created_at\":null,\"updated_at\":null,\"closed_at\":null,\"number\":null,\"state\":\"closed\"} 6 | 7 | HTTP/1.1 401 Unauthorized 8 | server: GitHub.com 9 | date: Mon, 05 Oct 2015 11:51:37 GMT 10 | content-type: application/json; charset=utf-8 11 | content-length: 83 12 | connection: close 13 | status: 401 Unauthorized 14 | x-github-media-type: github.v3 15 | x-ratelimit-limit: 60 16 | x-ratelimit-remaining: 58 17 | x-ratelimit-reset: 1444049497 18 | x-xss-protection: 1; mode=block 19 | x-frame-options: deny 20 | content-security-policy: default-src 'none' 21 | access-control-allow-credentials: true 22 | access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 23 | access-control-allow-origin: * 24 | x-github-request-id: 183D2EF0:17F5B:D99B4F1:56126449 25 | strict-transport-security: max-age=31536000; includeSubdomains; preload 26 | x-content-type-options: nosniff 27 | 28 | {"message":"Bad credentials","documentation_url":"https://developer.github.com/v3"} -------------------------------------------------------------------------------- /fixtures/tudddy.atlassian.net-443/143950153298946057: -------------------------------------------------------------------------------- 1 | POST /rest/api/2/issue 2 | host: tudddy.atlassian.net 3 | authorization: Basic YWRtaW46dHVkZHlwYXNz 4 | accept: application/json 5 | content-type: application/json 6 | body: {\"fields\":{\"summary\":\"test3\",\"project\":{\"key\":\"TUD\"},\"issuetype\":{\"name\":\"Task\"}}} 7 | 8 | HTTP/1.1 201 undefined 9 | server: nginx 10 | date: Thu, 13 Aug 2015 21:32:12 GMT 11 | content-type: application/json;charset=UTF-8 12 | transfer-encoding: chunked 13 | connection: keep-alive 14 | x-arequestid: 1052x984x2 15 | x-asen: SEN-5570487 16 | set-cookie: JSESSIONID=DBCE8D06267A232CA16F37E953F936CE; Path=/; Secure; HttpOnly 17 | set-cookie: studio.crowd.tokenkey=""; Domain=.tudddy.atlassian.net; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Path=/; Secure; HttpOnly 18 | set-cookie: studio.crowd.tokenkey=4r2QF0k3NV7S849fxMXHOQ00; Domain=.tudddy.atlassian.net; Path=/; Secure; HttpOnly 19 | set-cookie: atlassian.xsrf.token=B7HM-M1NA-RPNE-DB50|e5e277bde7f8f4887b973ddf70a48b6450ba9100|lin; Path=/; Secure 20 | x-seraph-loginreason: OUT, OK 21 | x-asessionid: 1yo43lb 22 | x-ausername: admin 23 | x-atenant-id: tudddy.atlassian.net 24 | cache-control: no-cache, no-store, no-transform 25 | x-content-type-options: nosniff 26 | strict-transport-security: max-age=315360000;includeSubDomains 27 | 28 | {"id":"10036","key":"TUD-37","self":"https://tudddy.atlassian.net/rest/api/2/issue/10036"} -------------------------------------------------------------------------------- /fixtures/tudddy.atlassian.net-443/143950153299281116: -------------------------------------------------------------------------------- 1 | POST /rest/api/2/issue 2 | host: tudddy.atlassian.net 3 | authorization: Basic YWRtaW46dHVkZHlwYXNz 4 | accept: application/json 5 | content-type: application/json 6 | body: {\"fields\":{\"summary\":\"test4\",\"project\":{\"key\":\"TUD\"},\"issuetype\":{\"name\":\"Task\"}}} 7 | 8 | HTTP/1.1 201 undefined 9 | server: nginx 10 | date: Thu, 13 Aug 2015 21:32:12 GMT 11 | content-type: application/json;charset=UTF-8 12 | transfer-encoding: chunked 13 | connection: keep-alive 14 | x-arequestid: 1052x983x1 15 | x-asen: SEN-5570487 16 | set-cookie: JSESSIONID=CDBFB4B63EC11E6CADBE5D890A526D2B; Path=/; Secure; HttpOnly 17 | set-cookie: studio.crowd.tokenkey=""; Domain=.tudddy.atlassian.net; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Path=/; Secure; HttpOnly 18 | set-cookie: studio.crowd.tokenkey=4r2QF0k3NV7S849fxMXHOQ00; Domain=.tudddy.atlassian.net; Path=/; Secure; HttpOnly 19 | set-cookie: atlassian.xsrf.token=B7HM-M1NA-RPNE-DB50|626dce0c0be7d0afb1b17ec46f2ff7f5166a1dc0|lin; Path=/; Secure 20 | x-seraph-loginreason: OUT, OK 21 | x-asessionid: ffrtcb 22 | x-ausername: admin 23 | x-atenant-id: tudddy.atlassian.net 24 | cache-control: no-cache, no-store, no-transform 25 | x-content-type-options: nosniff 26 | strict-transport-security: max-age=315360000;includeSubDomains 27 | 28 | {"id":"10035","key":"TUD-36","self":"https://tudddy.atlassian.net/rest/api/2/issue/10035"} -------------------------------------------------------------------------------- /fixtures/sprint.ly-443/143956904868389120: -------------------------------------------------------------------------------- 1 | GET /api/products/33921/items.json 2 | authorization: Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG 3 | host: sprint.ly 4 | 5 | HTTP/1.1 200 undefined 6 | content-type: application/json 7 | date: Fri, 14 Aug 2015 16:17:28 GMT 8 | server: nginx/1.1.19 9 | strict-transport-security: max-age=604800 10 | x-frame-options: SAMEORIGIN 11 | x-sprintly-item-count: 1 12 | x-trace: 1BD35DE2D1F91801F09F4319CA79E38FFB24D4387B8D899C763531629B 13 | content-length: 939 14 | connection: keep-alive 15 | 16 | [{"status":"backlog","sort":90000,"product":{"archived":false,"id":33921,"name":"My First Product"},"description":"","tags":[],"short_url":"http://sprint.ly/i/33921/19/","who":"user","number":19,"deployed_to":[],"last_modified":"2015-08-14T15:13:16+00:00","counts":{"blockers":0,"blocking":0,"favorites":0,"comments":0},"why":"I can test the API","what":"a test item","title":"As a user, I want a test item so that I can test the API.","created_at":"2015-08-14T15:05:55+00:00","created_by":{"first_name":"Jeff","last_name":"Loiselle","created_at":"2015-08-14T14:34:10+00:00","account":{"active":true,"pk":20726,"created":"2015-08-14T14:34:11+00:00"},"email":"jeff+tuddy@loiselles.com","last_login":"2015-08-14T14:34:27+00:00","id":39098},"score":"~","assigned_to":null,"progress":{"triaged_at":"2015-08-14T15:13:16+00:00"},"type":"story","email":{"files":"files-1172834@items.sprint.ly","discussion":"discussion-1172834@items.sprint.ly"}}] -------------------------------------------------------------------------------- /fixtures/teuxdeux.com-443/14434432736474317: -------------------------------------------------------------------------------- 1 | POST /api/v1/todos 2 | x-csrf-token: d151f2a1c31d5b5374f64112be07c3cd637b9f8b6450fa54ee3dd492948eed0c 3 | host: teuxdeux.com 4 | content-type: application/x-www-form-urlencoded 5 | accept: application/json 6 | body: id=&text=test3&uuid=&done=false¤t_date=2015-09-11&created_at=&updated_at= 7 | 8 | HTTP/1.1 201 Created 9 | content-type: application/json; charset=UTF-8 10 | date: Mon, 28 Sep 2015 12:27:53 GMT 11 | server: nginx/1.2.1 12 | set-cookie: rack.session=BAh7CUkiD3Nlc3Npb25faWQGOgZFRiJFNTA4YWZiZDdlZTdjOTViOWQ2YThi%0AZDg0ZDI5ODI1MmFlYThmNzMwNjcwMjdmMThiMWRlYTgyYTg1NDI5ZmNlNEki%0ADl9fRkxBU0hfXwY7AEZ7AEkiCWNzcmYGOwBGIkVkMTUxZjJhMWMzMWQ1YjUz%0ANzRmNjQxMTJiZTA3YzNjZDYzN2I5ZjhiNjQ1MGZhNTRlZTNkZDQ5Mjk0OGVl%0AZDBjSSIMdXNlcl9pZAY7AEZpAizW%0A--fae485c6811e7ef3d7c07d25450f0f0e814f5570; path=/; expires=Wed, 28 Oct 2015 12:27:53 -0000; secure; HttpOnly 13 | status: 201 Created 14 | x-frame-options: sameorigin 15 | x-xss-protection: 1; mode=block 16 | content-length: 461 17 | connection: Close 18 | 19 | {"id":34800618,"cid":null,"uuid":"3ef6698d-2d53-4d4d-b11c-8066d1635320","text":"test3","position":1,"list_id":null,"start_date":"2015-09-11","current_date":"2015-09-11","done":false,"user_id":54828,"recurring_todo_id":null,"recurring":false,"created_at":"2015-09-28T12:27:53+00:00","updated_at":"2015-09-28T12:27:53+00:00","text_updated_at":null,"done_updated_at":null,"position_updated_at":null,"column_updated_at":null,"deleted_at":null,"recurring_todo":null} -------------------------------------------------------------------------------- /fixtures/teuxdeux.com-443/144344327365530891: -------------------------------------------------------------------------------- 1 | POST /api/v1/todos 2 | x-csrf-token: d151f2a1c31d5b5374f64112be07c3cd637b9f8b6450fa54ee3dd492948eed0c 3 | host: teuxdeux.com 4 | content-type: application/x-www-form-urlencoded 5 | accept: application/json 6 | body: id=&text=test4&uuid=&done=false¤t_date=2015-09-11&created_at=&updated_at= 7 | 8 | HTTP/1.1 201 Created 9 | content-type: application/json; charset=UTF-8 10 | date: Mon, 28 Sep 2015 12:27:53 GMT 11 | server: nginx/1.2.1 12 | set-cookie: rack.session=BAh7CUkiD3Nlc3Npb25faWQGOgZFRiJFNTA4YWZiZDdlZTdjOTViOWQ2YThi%0AZDg0ZDI5ODI1MmFlYThmNzMwNjcwMjdmMThiMWRlYTgyYTg1NDI5ZmNlNEki%0ADl9fRkxBU0hfXwY7AEZ7AEkiCWNzcmYGOwBGIkVkMTUxZjJhMWMzMWQ1YjUz%0ANzRmNjQxMTJiZTA3YzNjZDYzN2I5ZjhiNjQ1MGZhNTRlZTNkZDQ5Mjk0OGVl%0AZDBjSSIMdXNlcl9pZAY7AEZpAizW%0A--fae485c6811e7ef3d7c07d25450f0f0e814f5570; path=/; expires=Wed, 28 Oct 2015 12:27:53 -0000; secure; HttpOnly 13 | status: 201 Created 14 | x-frame-options: sameorigin 15 | x-xss-protection: 1; mode=block 16 | content-length: 461 17 | connection: Close 18 | 19 | {"id":34800619,"cid":null,"uuid":"a14aec8b-6c5f-475d-824a-1209223bce69","text":"test4","position":2,"list_id":null,"start_date":"2015-09-11","current_date":"2015-09-11","done":false,"user_id":54828,"recurring_todo_id":null,"recurring":false,"created_at":"2015-09-28T12:27:53+00:00","updated_at":"2015-09-28T12:27:53+00:00","text_updated_at":null,"done_updated_at":null,"position_updated_at":null,"column_updated_at":null,"deleted_at":null,"recurring_todo":null} -------------------------------------------------------------------------------- /fixtures/www.pivotaltracker.com-443/143950789799014555: -------------------------------------------------------------------------------- 1 | POST /services/v5/projects/1407870/stories 2 | x-trackertoken: 690eb36760eff666af11c563510fc616 3 | content-type: application/json 4 | host: www.pivotaltracker.com 5 | accept: application/json 6 | body: {\"name\":\"test1\"} 7 | 8 | HTTP/1.1 200 undefined 9 | content-type: application/json; charset=utf-8 10 | transfer-encoding: chunked 11 | status: 200 OK 12 | x-tracker-project-version: 7 13 | x-ua-compatible: IE=Edge,chrome=1 14 | etag: "11c917bc9f8c40c6a5838ca8a9b89f2c" 15 | cache-control: max-age=0, private, must-revalidate 16 | x-request-id: d8a31ad92dd6520bb99e7b39ec9d528c 17 | x-runtime: 0.195450 18 | date: Thu, 13 Aug 2015 23:18:17 GMT 19 | x-rack-cache: invalidate, pass 20 | x-powered-by: Phusion Passenger 4.0.41 21 | server: nginx/1.6.0 + Phusion Passenger 4.0.41 22 | access-control-allow-origin: * 23 | access-control-allow-credentials: false 24 | access-control-allow-methods: GET, POST, PUT, DELETE, OPTIONS 25 | access-control-allow-headers: X-TrackerToken,DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,X-Tracker-Warn-Unless-Project-Version-Is 26 | x-tracker-client-pinger-interval: 12 27 | 28 | {"kind":"story","id":101250806,"project_id":1407870,"name":"test1","story_type":"feature","current_state":"unscheduled","requested_by_id":1762546,"owner_ids":[],"labels":[],"created_at":"2015-08-13T23:18:17Z","updated_at":"2015-08-13T23:18:17Z","url":"https://www.pivotaltracker.com/story/show/101250806"} -------------------------------------------------------------------------------- /fixtures/www.pivotaltracker.com-443/143950789818378886: -------------------------------------------------------------------------------- 1 | POST /services/v5/projects/1407870/stories 2 | x-trackertoken: 690eb36760eff666af11c563510fc616 3 | content-type: application/json 4 | host: www.pivotaltracker.com 5 | accept: application/json 6 | body: {\"name\":\"test2\"} 7 | 8 | HTTP/1.1 200 undefined 9 | content-type: application/json; charset=utf-8 10 | transfer-encoding: chunked 11 | status: 200 OK 12 | x-tracker-project-version: 8 13 | x-ua-compatible: IE=Edge,chrome=1 14 | etag: "df4ca73b52afcb27faf0fe068281c9c3" 15 | cache-control: max-age=0, private, must-revalidate 16 | x-request-id: af092a454d009e9635c9e8642ad2b17f 17 | x-runtime: 0.393947 18 | date: Thu, 13 Aug 2015 23:18:18 GMT 19 | x-rack-cache: invalidate, pass 20 | x-powered-by: Phusion Passenger 4.0.41 21 | server: nginx/1.6.0 + Phusion Passenger 4.0.41 22 | access-control-allow-origin: * 23 | access-control-allow-credentials: false 24 | access-control-allow-methods: GET, POST, PUT, DELETE, OPTIONS 25 | access-control-allow-headers: X-TrackerToken,DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,X-Tracker-Warn-Unless-Project-Version-Is 26 | x-tracker-client-pinger-interval: 12 27 | 28 | {"kind":"story","id":101250808,"project_id":1407870,"name":"test2","story_type":"feature","current_state":"unscheduled","requested_by_id":1762546,"owner_ids":[],"labels":[],"created_at":"2015-08-13T23:18:17Z","updated_at":"2015-08-13T23:18:17Z","url":"https://www.pivotaltracker.com/story/show/101250808"} -------------------------------------------------------------------------------- /fixtures/www.pivotaltracker.com-443/143950726207840389: -------------------------------------------------------------------------------- 1 | POST /services/v5/projects/1407870/stories 2 | x-trackertoken: 690eb36760eff666af11c563510fc616 3 | content-type: application/json 4 | accept-encoding: gzip,deflate 5 | accept: */* 6 | body: {\"name\":\"test2\"} 7 | 8 | HTTP/1.1 200 undefined 9 | content-type: application/json; charset=utf-8 10 | transfer-encoding: chunked 11 | connection: close 12 | status: 200 OK 13 | x-tracker-project-version: 5 14 | x-ua-compatible: IE=Edge,chrome=1 15 | etag: "cc21a2cf40eabb62ed5eff74ca4afe90" 16 | cache-control: max-age=0, private, must-revalidate 17 | x-request-id: 42a656049e3a383c1fafcc907bb62494 18 | x-runtime: 0.192392 19 | date: Thu, 13 Aug 2015 23:07:42 GMT 20 | x-rack-cache: invalidate, pass 21 | x-powered-by: Phusion Passenger 4.0.41 22 | server: nginx/1.6.0 + Phusion Passenger 4.0.41 23 | access-control-allow-origin: * 24 | access-control-allow-credentials: false 25 | access-control-allow-methods: GET, POST, PUT, DELETE, OPTIONS 26 | access-control-allow-headers: X-TrackerToken,DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,X-Tracker-Warn-Unless-Project-Version-Is 27 | x-tracker-client-pinger-interval: 12 28 | 29 | {"kind":"story","id":101250266,"project_id":1407870,"name":"test2","story_type":"feature","current_state":"unscheduled","requested_by_id":1762546,"owner_ids":[],"labels":[],"created_at":"2015-08-13T23:07:41Z","updated_at":"2015-08-13T23:07:41Z","url":"https://www.pivotaltracker.com/story/show/101250266"} -------------------------------------------------------------------------------- /fixtures/www.pivotaltracker.com-443/143950726220617327: -------------------------------------------------------------------------------- 1 | POST /services/v5/projects/1407870/stories 2 | x-trackertoken: 690eb36760eff666af11c563510fc616 3 | content-type: application/json 4 | accept-encoding: gzip,deflate 5 | accept: */* 6 | body: {\"name\":\"test1\"} 7 | 8 | HTTP/1.1 200 undefined 9 | content-type: application/json; charset=utf-8 10 | transfer-encoding: chunked 11 | connection: close 12 | status: 200 OK 13 | x-tracker-project-version: 6 14 | x-ua-compatible: IE=Edge,chrome=1 15 | etag: "005c250a86d39fe3d89fc1c49d19df08" 16 | cache-control: max-age=0, private, must-revalidate 17 | x-request-id: 111fe7110adf4e8f48e4e8f9da7615ae 18 | x-runtime: 0.327978 19 | date: Thu, 13 Aug 2015 23:07:42 GMT 20 | x-rack-cache: invalidate, pass 21 | x-powered-by: Phusion Passenger 4.0.41 22 | server: nginx/1.6.0 + Phusion Passenger 4.0.41 23 | access-control-allow-origin: * 24 | access-control-allow-credentials: false 25 | access-control-allow-methods: GET, POST, PUT, DELETE, OPTIONS 26 | access-control-allow-headers: X-TrackerToken,DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,X-Tracker-Warn-Unless-Project-Version-Is 27 | x-tracker-client-pinger-interval: 12 28 | 29 | {"kind":"story","id":101250268,"project_id":1407870,"name":"test1","story_type":"feature","current_state":"unscheduled","requested_by_id":1762546,"owner_ids":[],"labels":[],"created_at":"2015-08-13T23:07:42Z","updated_at":"2015-08-13T23:07:42Z","url":"https://www.pivotaltracker.com/story/show/101250268"} -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var sinon = require('sinon'); 3 | 4 | var tuddy = require('../lib/tuddy'); 5 | 6 | describe('Tuddy', () => { 7 | describe('#constructor', () => { 8 | it('should create map of integrations', () => { 9 | var client = tuddy(); 10 | expect(client.integrations).to.be.a('map'); 11 | }); 12 | }); 13 | describe('#addIntegration', () => { 14 | it('should register an integration', () => { 15 | var client = tuddy(); 16 | client.addIntegration({ name: 'trello', type: 'trello', key: '', token: '', board_id: '' }); 17 | expect(client.integrations.get('trello')).to.be.a('object'); 18 | expect(client.integrations.size).to.eql(1); 19 | }); 20 | }); 21 | describe('#pull', () => { 22 | it('should load plugin and call pull proxy method', () => { 23 | var client = tuddy(); 24 | client.addIntegration({ name: 'trello', type: 'trello', board_id: '', key: '', token: '' }); 25 | 26 | var spy = sinon.spy(client.plugins.get('trello'), 'pull'); 27 | 28 | client.pull('trello'); 29 | expect(spy.called).to.be.true; 30 | }); 31 | }); 32 | describe('#push', () => { 33 | it('should load plugin and call push proxy method', () => { 34 | var client = tuddy(); 35 | client.addIntegration({ name: 'trello', type: 'trello', board_id: '', key: '', token: '' }); 36 | 37 | var spy = sinon.spy(client.plugins.get('trello'), 'push'); 38 | 39 | client.push('trello', []); 40 | expect(spy.called).to.be.true; 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /fixtures/sprint.ly-443/14440476357067901: -------------------------------------------------------------------------------- 1 | POST /api/products/33921/items.json 2 | authorization: Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG 3 | host: sprint.ly 4 | content-type: multipart/form-data; boundary=--------------------------657382669280477088079882 5 | body: ----------------------------657382669280477088079882\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\ntest4\r\n----------------------------657382669280477088079882\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\ntask\r\n----------------------------657382669280477088079882--\r\n 6 | 7 | HTTP/1.1 200 OK 8 | content-type: application/json 9 | date: Mon, 05 Oct 2015 12:20:35 GMT 10 | server: nginx/1.1.19 11 | strict-transport-security: max-age=604800 12 | x-frame-options: SAMEORIGIN 13 | content-length: 767 14 | connection: Close 15 | 16 | {"status":"someday","sort":60000,"product":{"archived":false,"id":33921,"name":"My First Product"},"description":"","tags":[],"short_url":"http://sprint.ly/i/33921/8/","number":8,"deployed_to":[],"last_modified":"2015-10-05T12:20:35+00:00","counts":{"blockers":0,"blocking":0,"favorites":0,"comments":0},"title":"test4","created_at":"2015-10-05T12:20:35+00:00","created_by":{"first_name":"Jeff","last_name":"Loiselle","created_at":"2015-08-14T14:34:10+00:00","account":{"active":true,"pk":20726,"created":"2015-08-14T14:34:11+00:00"},"email":"jeff+tuddy@loiselles.com","last_login":"2015-10-05T12:04:37+00:00","id":39098},"score":"~","assigned_to":null,"type":"task","email":{"files":"files-1214704@items.sprint.ly","discussion":"discussion-1214704@items.sprint.ly"}} -------------------------------------------------------------------------------- /fixtures/sprint.ly-443/144404746877869619: -------------------------------------------------------------------------------- 1 | POST /api/products/33921/items.json 2 | authorization: Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG 3 | host: sprint.ly 4 | content-type: multipart/form-data; boundary=--------------------------769363300333154817275706 5 | body: ----------------------------769363300333154817275706\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\ntest4\r\n----------------------------769363300333154817275706\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\ntask\r\n----------------------------769363300333154817275706--\r\n 6 | 7 | HTTP/1.1 200 OK 8 | content-type: application/json 9 | date: Mon, 05 Oct 2015 12:17:48 GMT 10 | server: nginx/1.1.19 11 | strict-transport-security: max-age=604800 12 | x-frame-options: SAMEORIGIN 13 | content-length: 767 14 | connection: Close 15 | 16 | {"status":"someday","sort":50000,"product":{"archived":false,"id":33921,"name":"My First Product"},"description":"","tags":[],"short_url":"http://sprint.ly/i/33921/6/","number":6,"deployed_to":[],"last_modified":"2015-10-05T12:17:48+00:00","counts":{"blockers":0,"blocking":0,"favorites":0,"comments":0},"title":"test4","created_at":"2015-10-05T12:17:48+00:00","created_by":{"first_name":"Jeff","last_name":"Loiselle","created_at":"2015-08-14T14:34:10+00:00","account":{"active":true,"pk":20726,"created":"2015-08-14T14:34:11+00:00"},"email":"jeff+tuddy@loiselles.com","last_login":"2015-10-05T12:04:37+00:00","id":39098},"score":"~","assigned_to":null,"type":"task","email":{"files":"files-1214701@items.sprint.ly","discussion":"discussion-1214701@items.sprint.ly"}} -------------------------------------------------------------------------------- /fixtures/sprint.ly-443/144404746878262524: -------------------------------------------------------------------------------- 1 | POST /api/products/33921/items.json 2 | authorization: Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG 3 | host: sprint.ly 4 | content-type: multipart/form-data; boundary=--------------------------003941013001474839841555 5 | body: ----------------------------003941013001474839841555\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\ntest3\r\n----------------------------003941013001474839841555\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\ntask\r\n----------------------------003941013001474839841555--\r\n 6 | 7 | HTTP/1.1 200 OK 8 | content-type: application/json 9 | date: Mon, 05 Oct 2015 12:17:48 GMT 10 | server: nginx/1.1.19 11 | strict-transport-security: max-age=604800 12 | x-frame-options: SAMEORIGIN 13 | content-length: 767 14 | connection: Close 15 | 16 | {"status":"someday","sort":50000,"product":{"archived":false,"id":33921,"name":"My First Product"},"description":"","tags":[],"short_url":"http://sprint.ly/i/33921/7/","number":7,"deployed_to":[],"last_modified":"2015-10-05T12:17:48+00:00","counts":{"blockers":0,"blocking":0,"favorites":0,"comments":0},"title":"test3","created_at":"2015-10-05T12:17:48+00:00","created_by":{"first_name":"Jeff","last_name":"Loiselle","created_at":"2015-08-14T14:34:10+00:00","account":{"active":true,"pk":20726,"created":"2015-08-14T14:34:11+00:00"},"email":"jeff+tuddy@loiselles.com","last_login":"2015-10-05T12:04:37+00:00","id":39098},"score":"~","assigned_to":null,"type":"task","email":{"files":"files-1214702@items.sprint.ly","discussion":"discussion-1214702@items.sprint.ly"}} -------------------------------------------------------------------------------- /fixtures/sprint.ly-443/144404763570338337: -------------------------------------------------------------------------------- 1 | POST /api/products/33921/items.json 2 | authorization: Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG 3 | host: sprint.ly 4 | content-type: multipart/form-data; boundary=--------------------------984477583153866195646815 5 | body: ----------------------------984477583153866195646815\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\ntest3\r\n----------------------------984477583153866195646815\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\ntask\r\n----------------------------984477583153866195646815--\r\n 6 | 7 | HTTP/1.1 200 OK 8 | content-type: application/json 9 | date: Mon, 05 Oct 2015 12:20:35 GMT 10 | server: nginx/1.1.19 11 | strict-transport-security: max-age=604800 12 | x-frame-options: SAMEORIGIN 13 | content-length: 767 14 | connection: Close 15 | 16 | {"status":"someday","sort":60000,"product":{"archived":false,"id":33921,"name":"My First Product"},"description":"","tags":[],"short_url":"http://sprint.ly/i/33921/9/","number":9,"deployed_to":[],"last_modified":"2015-10-05T12:20:35+00:00","counts":{"blockers":0,"blocking":0,"favorites":0,"comments":0},"title":"test3","created_at":"2015-10-05T12:20:35+00:00","created_by":{"first_name":"Jeff","last_name":"Loiselle","created_at":"2015-08-14T14:34:10+00:00","account":{"active":true,"pk":20726,"created":"2015-08-14T14:34:11+00:00"},"email":"jeff+tuddy@loiselles.com","last_login":"2015-10-05T12:04:37+00:00","id":39098},"score":"~","assigned_to":null,"type":"task","email":{"files":"files-1214705@items.sprint.ly","discussion":"discussion-1214705@items.sprint.ly"}} -------------------------------------------------------------------------------- /fixtures/sprint.ly-443/144404769950086401: -------------------------------------------------------------------------------- 1 | POST /api/products/33921/items.json 2 | authorization: Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG 3 | host: sprint.ly 4 | content-type: multipart/form-data; boundary=--------------------------480976856058191936388893 5 | body: ----------------------------480976856058191936388893\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\ntest4\r\n----------------------------480976856058191936388893\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\ntask\r\n----------------------------480976856058191936388893--\r\n 6 | 7 | HTTP/1.1 200 OK 8 | content-type: application/json 9 | date: Mon, 05 Oct 2015 12:21:39 GMT 10 | server: nginx/1.1.19 11 | strict-transport-security: max-age=604800 12 | x-frame-options: SAMEORIGIN 13 | content-length: 769 14 | connection: Close 15 | 16 | {"status":"someday","sort":70000,"product":{"archived":false,"id":33921,"name":"My First Product"},"description":"","tags":[],"short_url":"http://sprint.ly/i/33921/10/","number":10,"deployed_to":[],"last_modified":"2015-10-05T12:21:39+00:00","counts":{"blockers":0,"blocking":0,"favorites":0,"comments":0},"title":"test4","created_at":"2015-10-05T12:21:39+00:00","created_by":{"first_name":"Jeff","last_name":"Loiselle","created_at":"2015-08-14T14:34:10+00:00","account":{"active":true,"pk":20726,"created":"2015-08-14T14:34:11+00:00"},"email":"jeff+tuddy@loiselles.com","last_login":"2015-10-05T12:04:37+00:00","id":39098},"score":"~","assigned_to":null,"type":"task","email":{"files":"files-1214706@items.sprint.ly","discussion":"discussion-1214706@items.sprint.ly"}} -------------------------------------------------------------------------------- /fixtures/sprint.ly-443/144404769953725668: -------------------------------------------------------------------------------- 1 | POST /api/products/33921/items.json 2 | authorization: Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG 3 | host: sprint.ly 4 | content-type: multipart/form-data; boundary=--------------------------354483926579546209512443 5 | body: ----------------------------354483926579546209512443\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\ntest3\r\n----------------------------354483926579546209512443\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\ntask\r\n----------------------------354483926579546209512443--\r\n 6 | 7 | HTTP/1.1 200 OK 8 | content-type: application/json 9 | date: Mon, 05 Oct 2015 12:21:39 GMT 10 | server: nginx/1.1.19 11 | strict-transport-security: max-age=604800 12 | x-frame-options: SAMEORIGIN 13 | content-length: 769 14 | connection: Close 15 | 16 | {"status":"someday","sort":80000,"product":{"archived":false,"id":33921,"name":"My First Product"},"description":"","tags":[],"short_url":"http://sprint.ly/i/33921/11/","number":11,"deployed_to":[],"last_modified":"2015-10-05T12:21:39+00:00","counts":{"blockers":0,"blocking":0,"favorites":0,"comments":0},"title":"test3","created_at":"2015-10-05T12:21:39+00:00","created_by":{"first_name":"Jeff","last_name":"Loiselle","created_at":"2015-08-14T14:34:10+00:00","account":{"active":true,"pk":20726,"created":"2015-08-14T14:34:11+00:00"},"email":"jeff+tuddy@loiselles.com","last_login":"2015-10-05T12:04:37+00:00","id":39098},"score":"~","assigned_to":null,"type":"task","email":{"files":"files-1214707@items.sprint.ly","discussion":"discussion-1214707@items.sprint.ly"}} -------------------------------------------------------------------------------- /fixtures/sprint.ly-443/14440479455932645: -------------------------------------------------------------------------------- 1 | POST /api/products/33921/items.json 2 | authorization: Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG 3 | host: sprint.ly 4 | content-type: multipart/form-data; boundary=--------------------------213014414178902379065428 5 | body: ----------------------------213014414178902379065428\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\ntest4\r\n----------------------------213014414178902379065428\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\ntask\r\n----------------------------213014414178902379065428--\r\n 6 | 7 | HTTP/1.1 200 OK 8 | content-type: application/json 9 | date: Mon, 05 Oct 2015 12:25:45 GMT 10 | server: nginx/1.1.19 11 | strict-transport-security: max-age=604800 12 | x-frame-options: SAMEORIGIN 13 | content-length: 769 14 | connection: Close 15 | 16 | {"status":"someday","sort":90000,"product":{"archived":false,"id":33921,"name":"My First Product"},"description":"","tags":[],"short_url":"http://sprint.ly/i/33921/13/","number":13,"deployed_to":[],"last_modified":"2015-10-05T12:25:45+00:00","counts":{"blockers":0,"blocking":0,"favorites":0,"comments":0},"title":"test4","created_at":"2015-10-05T12:25:45+00:00","created_by":{"first_name":"Jeff","last_name":"Loiselle","created_at":"2015-08-14T14:34:10+00:00","account":{"active":true,"pk":20726,"created":"2015-08-14T14:34:11+00:00"},"email":"jeff+tuddy@loiselles.com","last_login":"2015-10-05T12:04:37+00:00","id":39098},"score":"~","assigned_to":null,"type":"task","email":{"files":"files-1214710@items.sprint.ly","discussion":"discussion-1214710@items.sprint.ly"}} -------------------------------------------------------------------------------- /fixtures/sprint.ly-443/14440479456231649: -------------------------------------------------------------------------------- 1 | POST /api/products/33921/items.json 2 | authorization: Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG 3 | host: sprint.ly 4 | content-type: multipart/form-data; boundary=--------------------------972217612211760076496901 5 | body: ----------------------------972217612211760076496901\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\ntest3\r\n----------------------------972217612211760076496901\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\ntask\r\n----------------------------972217612211760076496901--\r\n 6 | 7 | HTTP/1.1 200 OK 8 | content-type: application/json 9 | date: Mon, 05 Oct 2015 12:25:45 GMT 10 | server: nginx/1.1.19 11 | strict-transport-security: max-age=604800 12 | x-frame-options: SAMEORIGIN 13 | content-length: 769 14 | connection: Close 15 | 16 | {"status":"someday","sort":90000,"product":{"archived":false,"id":33921,"name":"My First Product"},"description":"","tags":[],"short_url":"http://sprint.ly/i/33921/12/","number":12,"deployed_to":[],"last_modified":"2015-10-05T12:25:45+00:00","counts":{"blockers":0,"blocking":0,"favorites":0,"comments":0},"title":"test3","created_at":"2015-10-05T12:25:45+00:00","created_by":{"first_name":"Jeff","last_name":"Loiselle","created_at":"2015-08-14T14:34:10+00:00","account":{"active":true,"pk":20726,"created":"2015-08-14T14:34:11+00:00"},"email":"jeff+tuddy@loiselles.com","last_login":"2015-10-05T12:04:37+00:00","id":39098},"score":"~","assigned_to":null,"type":"task","email":{"files":"files-1214709@items.sprint.ly","discussion":"discussion-1214709@items.sprint.ly"}} -------------------------------------------------------------------------------- /fixtures/sprint.ly-443/144404896070044788: -------------------------------------------------------------------------------- 1 | POST /api/products/33921/items.json 2 | authorization: Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG 3 | host: sprint.ly 4 | content-type: multipart/form-data; boundary=--------------------------322494094711230094317554 5 | body: ----------------------------322494094711230094317554\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\ntest4\r\n----------------------------322494094711230094317554\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\ntask\r\n----------------------------322494094711230094317554--\r\n 6 | 7 | HTTP/1.1 200 OK 8 | content-type: application/json 9 | date: Mon, 05 Oct 2015 12:42:40 GMT 10 | server: nginx/1.1.19 11 | strict-transport-security: max-age=604800 12 | x-frame-options: SAMEORIGIN 13 | content-length: 770 14 | connection: Close 15 | 16 | {"status":"someday","sort":100000,"product":{"archived":false,"id":33921,"name":"My First Product"},"description":"","tags":[],"short_url":"http://sprint.ly/i/33921/15/","number":15,"deployed_to":[],"last_modified":"2015-10-05T12:42:40+00:00","counts":{"blockers":0,"blocking":0,"favorites":0,"comments":0},"title":"test4","created_at":"2015-10-05T12:42:40+00:00","created_by":{"first_name":"Jeff","last_name":"Loiselle","created_at":"2015-08-14T14:34:10+00:00","account":{"active":true,"pk":20726,"created":"2015-08-14T14:34:11+00:00"},"email":"jeff+tuddy@loiselles.com","last_login":"2015-10-05T12:04:37+00:00","id":39098},"score":"~","assigned_to":null,"type":"task","email":{"files":"files-1214723@items.sprint.ly","discussion":"discussion-1214723@items.sprint.ly"}} -------------------------------------------------------------------------------- /fixtures/sprint.ly-443/144404896075932098: -------------------------------------------------------------------------------- 1 | POST /api/products/33921/items.json 2 | authorization: Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG 3 | host: sprint.ly 4 | content-type: multipart/form-data; boundary=--------------------------245507574522409133560954 5 | body: ----------------------------245507574522409133560954\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\ntest3\r\n----------------------------245507574522409133560954\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\ntask\r\n----------------------------245507574522409133560954--\r\n 6 | 7 | HTTP/1.1 200 OK 8 | content-type: application/json 9 | date: Mon, 05 Oct 2015 12:42:40 GMT 10 | server: nginx/1.1.19 11 | strict-transport-security: max-age=604800 12 | x-frame-options: SAMEORIGIN 13 | content-length: 770 14 | connection: Close 15 | 16 | {"status":"someday","sort":100000,"product":{"archived":false,"id":33921,"name":"My First Product"},"description":"","tags":[],"short_url":"http://sprint.ly/i/33921/14/","number":14,"deployed_to":[],"last_modified":"2015-10-05T12:42:40+00:00","counts":{"blockers":0,"blocking":0,"favorites":0,"comments":0},"title":"test3","created_at":"2015-10-05T12:42:40+00:00","created_by":{"first_name":"Jeff","last_name":"Loiselle","created_at":"2015-08-14T14:34:10+00:00","account":{"active":true,"pk":20726,"created":"2015-08-14T14:34:11+00:00"},"email":"jeff+tuddy@loiselles.com","last_login":"2015-10-05T12:04:37+00:00","id":39098},"score":"~","assigned_to":null,"type":"task","email":{"files":"files-1214724@items.sprint.ly","discussion":"discussion-1214724@items.sprint.ly"}} -------------------------------------------------------------------------------- /fixtures/sprint.ly-443/14440489702863834: -------------------------------------------------------------------------------- 1 | POST /api/products/33921/items.json 2 | authorization: Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG 3 | host: sprint.ly 4 | content-type: multipart/form-data; boundary=--------------------------877907643631103221002159 5 | body: ----------------------------877907643631103221002159\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\ntest4\r\n----------------------------877907643631103221002159\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\ntask\r\n----------------------------877907643631103221002159--\r\n 6 | 7 | HTTP/1.1 200 OK 8 | content-type: application/json 9 | date: Mon, 05 Oct 2015 12:42:50 GMT 10 | server: nginx/1.1.19 11 | strict-transport-security: max-age=604800 12 | x-frame-options: SAMEORIGIN 13 | content-length: 770 14 | connection: Close 15 | 16 | {"status":"someday","sort":110000,"product":{"archived":false,"id":33921,"name":"My First Product"},"description":"","tags":[],"short_url":"http://sprint.ly/i/33921/16/","number":16,"deployed_to":[],"last_modified":"2015-10-05T12:42:49+00:00","counts":{"blockers":0,"blocking":0,"favorites":0,"comments":0},"title":"test4","created_at":"2015-10-05T12:42:49+00:00","created_by":{"first_name":"Jeff","last_name":"Loiselle","created_at":"2015-08-14T14:34:10+00:00","account":{"active":true,"pk":20726,"created":"2015-08-14T14:34:11+00:00"},"email":"jeff+tuddy@loiselles.com","last_login":"2015-10-05T12:04:37+00:00","id":39098},"score":"~","assigned_to":null,"type":"task","email":{"files":"files-1214725@items.sprint.ly","discussion":"discussion-1214725@items.sprint.ly"}} -------------------------------------------------------------------------------- /fixtures/sprint.ly-443/144404897029563543: -------------------------------------------------------------------------------- 1 | POST /api/products/33921/items.json 2 | authorization: Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG 3 | host: sprint.ly 4 | content-type: multipart/form-data; boundary=--------------------------422289244180488636481065 5 | body: ----------------------------422289244180488636481065\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\ntest3\r\n----------------------------422289244180488636481065\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\ntask\r\n----------------------------422289244180488636481065--\r\n 6 | 7 | HTTP/1.1 200 OK 8 | content-type: application/json 9 | date: Mon, 05 Oct 2015 12:42:50 GMT 10 | server: nginx/1.1.19 11 | strict-transport-security: max-age=604800 12 | x-frame-options: SAMEORIGIN 13 | content-length: 770 14 | connection: Close 15 | 16 | {"status":"someday","sort":110000,"product":{"archived":false,"id":33921,"name":"My First Product"},"description":"","tags":[],"short_url":"http://sprint.ly/i/33921/17/","number":17,"deployed_to":[],"last_modified":"2015-10-05T12:42:49+00:00","counts":{"blockers":0,"blocking":0,"favorites":0,"comments":0},"title":"test3","created_at":"2015-10-05T12:42:49+00:00","created_by":{"first_name":"Jeff","last_name":"Loiselle","created_at":"2015-08-14T14:34:10+00:00","account":{"active":true,"pk":20726,"created":"2015-08-14T14:34:11+00:00"},"email":"jeff+tuddy@loiselles.com","last_login":"2015-10-05T12:04:37+00:00","id":39098},"score":"~","assigned_to":null,"type":"task","email":{"files":"files-1214726@items.sprint.ly","discussion":"discussion-1214726@items.sprint.ly"}} -------------------------------------------------------------------------------- /fixtures/sprint.ly-443/144404897663516032: -------------------------------------------------------------------------------- 1 | POST /api/products/33921/items.json 2 | authorization: Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG 3 | host: sprint.ly 4 | content-type: multipart/form-data; boundary=--------------------------066827799391299042345083 5 | body: ----------------------------066827799391299042345083\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\ntest4\r\n----------------------------066827799391299042345083\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\ntask\r\n----------------------------066827799391299042345083--\r\n 6 | 7 | HTTP/1.1 200 OK 8 | content-type: application/json 9 | date: Mon, 05 Oct 2015 12:42:56 GMT 10 | server: nginx/1.1.19 11 | strict-transport-security: max-age=604800 12 | x-frame-options: SAMEORIGIN 13 | content-length: 770 14 | connection: Close 15 | 16 | {"status":"someday","sort":120000,"product":{"archived":false,"id":33921,"name":"My First Product"},"description":"","tags":[],"short_url":"http://sprint.ly/i/33921/18/","number":18,"deployed_to":[],"last_modified":"2015-10-05T12:42:56+00:00","counts":{"blockers":0,"blocking":0,"favorites":0,"comments":0},"title":"test4","created_at":"2015-10-05T12:42:56+00:00","created_by":{"first_name":"Jeff","last_name":"Loiselle","created_at":"2015-08-14T14:34:10+00:00","account":{"active":true,"pk":20726,"created":"2015-08-14T14:34:11+00:00"},"email":"jeff+tuddy@loiselles.com","last_login":"2015-10-05T12:04:37+00:00","id":39098},"score":"~","assigned_to":null,"type":"task","email":{"files":"files-1214727@items.sprint.ly","discussion":"discussion-1214727@items.sprint.ly"}} -------------------------------------------------------------------------------- /fixtures/sprint.ly-443/144404897665374750: -------------------------------------------------------------------------------- 1 | POST /api/products/33921/items.json 2 | authorization: Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG 3 | host: sprint.ly 4 | content-type: multipart/form-data; boundary=--------------------------001551396271195112969209 5 | body: ----------------------------001551396271195112969209\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\ntest3\r\n----------------------------001551396271195112969209\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\ntask\r\n----------------------------001551396271195112969209--\r\n 6 | 7 | HTTP/1.1 200 OK 8 | content-type: application/json 9 | date: Mon, 05 Oct 2015 12:42:56 GMT 10 | server: nginx/1.1.19 11 | strict-transport-security: max-age=604800 12 | x-frame-options: SAMEORIGIN 13 | content-length: 770 14 | connection: Close 15 | 16 | {"status":"someday","sort":130000,"product":{"archived":false,"id":33921,"name":"My First Product"},"description":"","tags":[],"short_url":"http://sprint.ly/i/33921/23/","number":23,"deployed_to":[],"last_modified":"2015-10-05T12:42:56+00:00","counts":{"blockers":0,"blocking":0,"favorites":0,"comments":0},"title":"test3","created_at":"2015-10-05T12:42:56+00:00","created_by":{"first_name":"Jeff","last_name":"Loiselle","created_at":"2015-08-14T14:34:10+00:00","account":{"active":true,"pk":20726,"created":"2015-08-14T14:34:11+00:00"},"email":"jeff+tuddy@loiselles.com","last_login":"2015-10-05T12:04:37+00:00","id":39098},"score":"~","assigned_to":null,"type":"task","email":{"files":"files-1214728@items.sprint.ly","discussion":"discussion-1214728@items.sprint.ly"}} -------------------------------------------------------------------------------- /fixtures/sprint.ly-443/144404903034771246: -------------------------------------------------------------------------------- 1 | POST /api/products/33921/items.json 2 | authorization: Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG 3 | host: sprint.ly 4 | content-type: multipart/form-data; boundary=--------------------------972214991890478353178556 5 | body: ----------------------------972214991890478353178556\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\ntest3\r\n----------------------------972214991890478353178556\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\ntask\r\n----------------------------972214991890478353178556--\r\n 6 | 7 | HTTP/1.1 200 OK 8 | content-type: application/json 9 | date: Mon, 05 Oct 2015 12:43:50 GMT 10 | server: nginx/1.1.19 11 | strict-transport-security: max-age=604800 12 | x-frame-options: SAMEORIGIN 13 | content-length: 770 14 | connection: Close 15 | 16 | {"status":"someday","sort":140000,"product":{"archived":false,"id":33921,"name":"My First Product"},"description":"","tags":[],"short_url":"http://sprint.ly/i/33921/24/","number":24,"deployed_to":[],"last_modified":"2015-10-05T12:43:50+00:00","counts":{"blockers":0,"blocking":0,"favorites":0,"comments":0},"title":"test3","created_at":"2015-10-05T12:43:50+00:00","created_by":{"first_name":"Jeff","last_name":"Loiselle","created_at":"2015-08-14T14:34:10+00:00","account":{"active":true,"pk":20726,"created":"2015-08-14T14:34:11+00:00"},"email":"jeff+tuddy@loiselles.com","last_login":"2015-10-05T12:04:37+00:00","id":39098},"score":"~","assigned_to":null,"type":"task","email":{"files":"files-1214730@items.sprint.ly","discussion":"discussion-1214730@items.sprint.ly"}} -------------------------------------------------------------------------------- /fixtures/sprint.ly-443/144404903039163197: -------------------------------------------------------------------------------- 1 | POST /api/products/33921/items.json 2 | authorization: Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG 3 | host: sprint.ly 4 | content-type: multipart/form-data; boundary=--------------------------463246141635923322282796 5 | body: ----------------------------463246141635923322282796\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\ntest4\r\n----------------------------463246141635923322282796\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\ntask\r\n----------------------------463246141635923322282796--\r\n 6 | 7 | HTTP/1.1 200 OK 8 | content-type: application/json 9 | date: Mon, 05 Oct 2015 12:43:50 GMT 10 | server: nginx/1.1.19 11 | strict-transport-security: max-age=604800 12 | x-frame-options: SAMEORIGIN 13 | content-length: 770 14 | connection: Close 15 | 16 | {"status":"someday","sort":140000,"product":{"archived":false,"id":33921,"name":"My First Product"},"description":"","tags":[],"short_url":"http://sprint.ly/i/33921/25/","number":25,"deployed_to":[],"last_modified":"2015-10-05T12:43:50+00:00","counts":{"blockers":0,"blocking":0,"favorites":0,"comments":0},"title":"test4","created_at":"2015-10-05T12:43:50+00:00","created_by":{"first_name":"Jeff","last_name":"Loiselle","created_at":"2015-08-14T14:34:10+00:00","account":{"active":true,"pk":20726,"created":"2015-08-14T14:34:11+00:00"},"email":"jeff+tuddy@loiselles.com","last_login":"2015-10-05T12:04:37+00:00","id":39098},"score":"~","assigned_to":null,"type":"task","email":{"files":"files-1214731@items.sprint.ly","discussion":"discussion-1214731@items.sprint.ly"}} -------------------------------------------------------------------------------- /lib/tuddy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Function that returns an instance of client 3 | * @return {instance} Instance of client 4 | */ 5 | function tuddy() { 6 | return new Client(); 7 | } 8 | 9 | /** 10 | * Manages pushing, pulling, and syncing from multiple integrations 11 | */ 12 | class Client { 13 | constructor() { 14 | this.plugins = new Map(); 15 | } 16 | /** 17 | * Adds an integration by adding an instantiated plugin to the Map 18 | * @param {object} integration Integration name, type, and authentication info 19 | */ 20 | addIntegration(integration) { 21 | try { 22 | var plugin = require(`./plugins/${integration.type}`); 23 | } catch (err) { 24 | throw new Error(`${integration.type} is not a valid plugin`); 25 | } 26 | this.plugins.set(integration.name, new plugin(integration)); 27 | } 28 | /** 29 | * Returns an integration by name, throwing an error if it doesn't exist 30 | * @param {string} integrationName Integration identifier 31 | * @return {instance} Instance of plugin 32 | */ 33 | getIntegration(integrationName) { 34 | if (!this.plugins.has(integrationName)) { 35 | throw new Error(`"${integrationName}" is not a registered integration`); 36 | } 37 | return this.plugins.get(integrationName); 38 | } 39 | /** 40 | * Retrieve all stories from Integration 41 | * @param {string} integrationName Integration identifier 42 | * @return {instance} Instance of plugin 43 | */ 44 | pull(integrationName) { 45 | let instance = this.getIntegration(integrationName); 46 | return instance.pull(); 47 | } 48 | /** 49 | * Creates all stories on integration 50 | * @param {string} integrationName Integration identifier 51 | * @param {array} stories Array of stories 52 | * @return {instance} Instance of plugin 53 | */ 54 | push(integrationName, stories) { 55 | let instance = this.getIntegration(integrationName); 56 | return instance.push(stories); 57 | } 58 | } 59 | 60 | module.exports = tuddy; 61 | -------------------------------------------------------------------------------- /fixtures/api.trello.com-443/143948376866963411: -------------------------------------------------------------------------------- 1 | POST /1/cards 2 | host: api.trello.com 3 | accept: application/json 4 | content-type: application/json 5 | body: {\"name\":\"test1\",\"idList\":\"55ccc4a5e08262688ef56a96\",\"key\":\"87105c2d4268dd05af77c83b64326f8a\",\"token\":\"e6691ee3114053746f53504df00a736407b985ce3cd0b2957012fa702ce83065\"} 6 | 7 | HTTP/1.1 200 undefined 8 | cache-control: max-age=0, must-revalidate, no-cache, no-store 9 | x-content-type-options: nosniff 10 | strict-transport-security: max-age=15768000 11 | x-xss-protection: 1; mode=block 12 | x-frame-options: DENY 13 | x-trello-version: 1.452.0 14 | x-trello-environment: Production 15 | access-control-allow-origin: * 16 | access-control-allow-methods: GET, PUT, POST, DELETE 17 | x-server-time: 1439483768609 18 | expires: Thu, 01 Jan 1970 00:00:00 19 | content-type: application/json; charset=utf-8 20 | content-length: 793 21 | etag: W/"319-d46abcd" 22 | vary: Accept-Encoding 23 | date: Thu, 13 Aug 2015 16:36:08 GMT 24 | set-cookie: visid_incap_168562=WGje5/M4SjqYizzcBJsFpHfHzFUAAAAAQUIPAAAAAAC8xf4wfd982x52Q0XPyfJd; expires=Sat, 12 Aug 2017 09:23:05 GMT; path=/; Domain=.trello.com 25 | set-cookie: incap_ses_222_168562=KAyJQXJkrmR5yKzYg7QUA3fHzFUAAAAAKMpSfRiIPAhgVFsw8tzq7g==; path=/; Domain=.trello.com 26 | x-iinfo: 4-3296011-3276091 PNNN RT(1439483767325 132) q(0 0 0 -1) r(3 3) U6 27 | x-cdn: Incapsula 28 | 29 | {"id":"55ccc778c605511b4dafd67d","badges":{"votes":0,"viewingMemberVoted":false,"subscribed":false,"fogbugz":"","checkItems":0,"checkItemsChecked":0,"comments":0,"attachments":0,"description":false,"due":null},"checkItemStates":[],"closed":false,"dateLastActivity":"2015-08-13T16:36:08.527Z","desc":"","descData":{"emoji":{}},"due":null,"email":"tuddy2+55ccc162f6f892ec7d80b27f+55ccc778c605511b4dafd67d+cb542b213ba83706f4e9c8c3a691b4b43c73abec@boards.trello.com","idBoard":"55ccc49df22b549c64f3b91e","idChecklists":[],"idLabels":[],"idList":"55ccc4a5e08262688ef56a96","idMembers":[],"idShort":3,"idAttachmentCover":null,"manualCoverAttachment":false,"labels":[],"name":"test1","pos":147455,"shortUrl":"https://trello.com/c/WoSysVwC","url":"https://trello.com/c/WoSysVwC/3-test1","stickers":[]} -------------------------------------------------------------------------------- /fixtures/api.trello.com-443/143948377010915063: -------------------------------------------------------------------------------- 1 | POST /1/cards 2 | host: api.trello.com 3 | accept: application/json 4 | content-type: application/json 5 | body: {\"name\":\"test2\",\"idList\":\"55ccc4a5e08262688ef56a96\",\"key\":\"87105c2d4268dd05af77c83b64326f8a\",\"token\":\"e6691ee3114053746f53504df00a736407b985ce3cd0b2957012fa702ce83065\"} 6 | 7 | HTTP/1.1 200 undefined 8 | cache-control: max-age=0, must-revalidate, no-cache, no-store 9 | x-content-type-options: nosniff 10 | strict-transport-security: max-age=15768000 11 | x-xss-protection: 1; mode=block 12 | x-frame-options: DENY 13 | x-trello-version: 1.452.0 14 | x-trello-environment: Production 15 | access-control-allow-origin: * 16 | access-control-allow-methods: GET, PUT, POST, DELETE 17 | x-server-time: 1439483770058 18 | expires: Thu, 01 Jan 1970 00:00:00 19 | content-type: application/json; charset=utf-8 20 | content-length: 793 21 | etag: W/"319-db080443" 22 | vary: Accept-Encoding 23 | date: Thu, 13 Aug 2015 16:36:10 GMT 24 | set-cookie: visid_incap_168562=suRZuHt/RZKmaP9lVGEYXnnHzFUAAAAAQUIPAAAAAADCyZFJhdH0Zzh3XZpy4PVw; expires=Sat, 12 Aug 2017 09:23:05 GMT; path=/; Domain=.trello.com 25 | set-cookie: incap_ses_222_168562=57vzAxf4WyurzazYg7QUA3nHzFUAAAAAEO5M5o04SWX8bEpzoiSJ8A==; path=/; Domain=.trello.com 26 | x-iinfo: 5-4400947-4399274 PNNN RT(1439483767326 132) q(0 0 0 -1) r(17 17) U6 27 | x-cdn: Incapsula 28 | 29 | {"id":"55ccc778590aa7435ca2064d","badges":{"votes":0,"viewingMemberVoted":false,"subscribed":false,"fogbugz":"","checkItems":0,"checkItemsChecked":0,"comments":0,"attachments":0,"description":false,"due":null},"checkItemStates":[],"closed":false,"dateLastActivity":"2015-08-13T16:36:08.776Z","desc":"","descData":{"emoji":{}},"due":null,"email":"tuddy2+55ccc162f6f892ec7d80b27f+55ccc778590aa7435ca2064d+f0be33478614f53669fcfc4e4cc43fc9c6fe06d5@boards.trello.com","idBoard":"55ccc49df22b549c64f3b91e","idChecklists":[],"idLabels":[],"idList":"55ccc4a5e08262688ef56a96","idMembers":[],"idShort":4,"idAttachmentCover":null,"manualCoverAttachment":false,"labels":[],"name":"test2","pos":163839,"shortUrl":"https://trello.com/c/6kcAzvRV","url":"https://trello.com/c/6kcAzvRV/4-test2","stickers":[]} -------------------------------------------------------------------------------- /dist/scsf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 6 | 7 | var _ = require('lodash'); 8 | 9 | var SCSF = (function () { 10 | function SCSF() { 11 | _classCallCheck(this, SCSF); 12 | } 13 | 14 | _createClass(SCSF, null, [{ 15 | key: 'merge', 16 | value: function merge(data) { 17 | var schema = SCSF.schema(); 18 | return _.merge(schema, data); 19 | } 20 | }, { 21 | key: 'schema', 22 | value: function schema() { 23 | return { 24 | meta: { 25 | source: { 26 | id: null, 27 | name: null, 28 | key: null, 29 | data: null 30 | } 31 | }, 32 | data: { 33 | id: null, 34 | self: null, 35 | key: null, 36 | name: null, 37 | description: null, 38 | type: null, 39 | url: null, 40 | archived: null, 41 | status: null, 42 | email: null, 43 | short_url: null, 44 | date: { 45 | start: null, 46 | end: null, 47 | due: null, 48 | created: null, 49 | updated: null, 50 | completed: null, 51 | deleted: null 52 | }, 53 | lists: [], 54 | labels: [], 55 | project: { 56 | id: null 57 | }, 58 | document: { 59 | id: null, 60 | range: null 61 | }, 62 | users: [] 63 | } 64 | }; 65 | } 66 | }]); 67 | 68 | return SCSF; 69 | })(); 70 | 71 | module.exports = SCSF; -------------------------------------------------------------------------------- /fixtures/api.trello.com-443/144380891553252080: -------------------------------------------------------------------------------- 1 | POST /1/cards 2 | host: api.trello.com 3 | accept: application/json 4 | content-type: application/json 5 | body: {\"name\":\"test2\",\"shortLink\":null,\"url\":null,\"desc\":null,\"shortUrl\":null,\"email\":null,\"due\":null,\"dateLastActivity\":null,\"closed\":false,\"idList\":\"55ccc4a5e08262688ef56a96\",\"key\":\"87105c2d4268dd05af77c83b64326f8a\",\"token\":\"e6691ee3114053746f53504df00a736407b985ce3cd0b2957012fa702ce83065\"} 6 | 7 | HTTP/1.1 200 OK 8 | cache-control: max-age=0, must-revalidate, no-cache, no-store 9 | x-content-type-options: nosniff 10 | strict-transport-security: max-age=15768000 11 | x-xss-protection: 1; mode=block 12 | x-frame-options: DENY 13 | x-trello-version: 1.486.0 14 | x-trello-environment: Production 15 | access-control-allow-origin: * 16 | access-control-allow-methods: GET, PUT, POST, DELETE 17 | x-server-time: 1443808915455 18 | expires: Thu, 01 Jan 1970 00:00:00 19 | content-type: application/json; charset=utf-8 20 | content-length: 793 21 | etag: W/"319-e0ef85c5" 22 | vary: Accept-Encoding 23 | date: Fri, 02 Oct 2015 18:01:55 GMT 24 | connection: close 25 | set-cookie: visid_incap_168562=iRhc+q/0QEyhoRyz/xb4dZLGDlYAAAAAQUIPAAAAAAAxaAhektZ9A3AmBUrzROUh; expires=Sun, 01 Oct 2017 11:06:25 GMT; path=/; Domain=.trello.com 26 | set-cookie: incap_ses_163_168562=qbcaBQ1BOXWUfnV95hdDApLGDlYAAAAA21ZxfdqmF34LUjCWzd4iHg==; path=/; Domain=.trello.com 27 | x-iinfo: 4-31680586-31680589 NNNN CT(7 9 0) RT(1443808914581 72) q(0 0 0 -1) r(2 2) U6 28 | x-cdn: Incapsula 29 | 30 | {"id":"560ec69362efc95c0d861452","badges":{"votes":0,"viewingMemberVoted":false,"subscribed":false,"fogbugz":"","checkItems":0,"checkItemsChecked":0,"comments":0,"attachments":0,"description":false,"due":null},"checkItemStates":[],"closed":false,"dateLastActivity":"2015-10-02T18:01:55.396Z","desc":"","descData":{"emoji":{}},"due":null,"email":"tuddy2+55ccc162f6f892ec7d80b27f+560ec69362efc95c0d861452+8161dcbbd20c537245e921c9c358c81c714c5ef4@boards.trello.com","idBoard":"55ccc49df22b549c64f3b91e","idChecklists":[],"idLabels":[],"idList":"55ccc4a5e08262688ef56a96","idMembers":[],"idShort":6,"idAttachmentCover":null,"manualCoverAttachment":false,"labels":[],"name":"test2","pos":196607,"shortUrl":"https://trello.com/c/h8CJ9Cs1","url":"https://trello.com/c/h8CJ9Cs1/6-test2","stickers":[]} -------------------------------------------------------------------------------- /fixtures/api.trello.com-443/144380891561814173: -------------------------------------------------------------------------------- 1 | POST /1/cards 2 | host: api.trello.com 3 | accept: application/json 4 | content-type: application/json 5 | body: {\"name\":\"test1\",\"shortLink\":null,\"url\":null,\"desc\":null,\"shortUrl\":null,\"email\":null,\"due\":null,\"dateLastActivity\":null,\"closed\":false,\"idList\":\"55ccc4a5e08262688ef56a96\",\"key\":\"87105c2d4268dd05af77c83b64326f8a\",\"token\":\"e6691ee3114053746f53504df00a736407b985ce3cd0b2957012fa702ce83065\"} 6 | 7 | HTTP/1.1 200 OK 8 | cache-control: max-age=0, must-revalidate, no-cache, no-store 9 | x-content-type-options: nosniff 10 | strict-transport-security: max-age=15768000 11 | x-xss-protection: 1; mode=block 12 | x-frame-options: DENY 13 | x-trello-version: 1.486.0 14 | x-trello-environment: Production 15 | access-control-allow-origin: * 16 | access-control-allow-methods: GET, PUT, POST, DELETE 17 | x-server-time: 1443808915544 18 | expires: Thu, 01 Jan 1970 00:00:00 19 | content-type: application/json; charset=utf-8 20 | content-length: 793 21 | etag: W/"319-a8878d0b" 22 | vary: Accept-Encoding 23 | date: Fri, 02 Oct 2015 18:01:55 GMT 24 | connection: close 25 | set-cookie: visid_incap_168562=R6i08zNkSWq5jxFvtQ+v0ZLGDlYAAAAAQUIPAAAAAAAKfy/J/lGmRJqdl9AqfS2H; expires=Sun, 01 Oct 2017 11:06:21 GMT; path=/; Domain=.trello.com 26 | set-cookie: incap_ses_163_168562=tKz9bM5MuybFfnV95hdDApLGDlYAAAAAAVqdjsW57HNHLZDFdfwGmQ==; path=/; Domain=.trello.com 27 | x-iinfo: 9-70742651-70742668 NNNN CT(13 8 0) RT(1443808914569 86) q(0 0 0 -1) r(2 2) U6 28 | x-cdn: Incapsula 29 | 30 | {"id":"560ec6938c58269dca52d58d","badges":{"votes":0,"viewingMemberVoted":false,"subscribed":false,"fogbugz":"","checkItems":0,"checkItemsChecked":0,"comments":0,"attachments":0,"description":false,"due":null},"checkItemStates":[],"closed":false,"dateLastActivity":"2015-10-02T18:01:55.470Z","desc":"","descData":{"emoji":{}},"due":null,"email":"tuddy2+55ccc162f6f892ec7d80b27f+560ec6938c58269dca52d58d+210f5e1b97173758b9804a0beb5c8717cb8cbb2e@boards.trello.com","idBoard":"55ccc49df22b549c64f3b91e","idChecklists":[],"idLabels":[],"idList":"55ccc4a5e08262688ef56a96","idMembers":[],"idShort":7,"idAttachmentCover":null,"manualCoverAttachment":false,"labels":[],"name":"test1","pos":212991,"shortUrl":"https://trello.com/c/6lNCKtEx","url":"https://trello.com/c/6lNCKtEx/7-test1","stickers":[]} -------------------------------------------------------------------------------- /test/google.spec.js: -------------------------------------------------------------------------------- 1 | require('replay'); 2 | var expect = require('chai').expect; 3 | 4 | var Google = require('../lib/plugins/google.js'); 5 | 6 | describe('Google', () => { 7 | // describe('#constructor', () => { 8 | // it('should take integration from constructor and set it', () => { 9 | // let integration = { repo: '', user: '', access_token: '' }; 10 | // var Google = new Google(integration); 11 | // expect(Google.integration).to.eql(integration); 12 | // }); 13 | // it('should set headers', () => { 14 | // let integration = { repo: '', user: '', access_token: '' }; 15 | // var Google = new Google(integration); 16 | // let headers = { 17 | // 'User-Agent': 'Tuddy', 18 | // 'Content-Type': 'application/json' 19 | // }; 20 | // expect(Google.headers).to.eql(headers); 21 | // }); 22 | // }); 23 | // describe('#url', () => { 24 | // it('should generate an API url with querystring', () => { 25 | // let integration = { repo: 'tuddy', user: 'tudddy', access_token: 'c136002525ae2a82dec9109c304167226f8f7e7e' }; 26 | // var Google = new Google(integration); 27 | // var url = Google.url({ per_page: 'thing' }); 28 | // expect(url).to.eql('https://api.Google.com/repos/tudddy/tuddy/issues?per_page=thing'); 29 | // }); 30 | // }); 31 | // describe('#push', () => { 32 | // it('should be able to push stories', (done) => { 33 | // let integration = { repo: 'tuddy', user: 'tudddy', access_token: 'c136002525ae2a82dec9109c304167226f8f7e7e' }; 34 | // var Google = new Google(integration); 35 | // let stories = [ 36 | // { 37 | // name: 'test3' 38 | // }, 39 | // { 40 | // name: 'test4' 41 | // } 42 | // ]; 43 | // Google.push(stories).then((stories) => { 44 | // expect(stories).to.have.length(2); 45 | // done(); 46 | // }); 47 | // }); 48 | // }); 49 | describe('#pull', () => { 50 | it('should be able to pull stories', (done) => { 51 | let integration = { repo: 'tuddy', user: 'tudddy', access_token: 'c136002525ae2a82dec9109c304167226f8f7e7e' }; 52 | var Google = new Google(integration); 53 | Google.pull().then((stories) => { 54 | expect(stories).to.have.length(4); 55 | done(); 56 | }); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/sprintly.spec.js: -------------------------------------------------------------------------------- 1 | require('replay'); 2 | var expect = require('chai').expect; 3 | 4 | var Sprintly = require('../lib/plugins/sprintly.js'); 5 | 6 | describe('Sprintly', () => { 7 | // describe('#constructor', () => { 8 | // it('should take integration from constructor and set it', () => { 9 | // let integration = { repo: '', user: '', access_token: '' }; 10 | // var Sprintly = new Sprintly(integration); 11 | // expect(Sprintly.integration).to.eql(integration); 12 | // }); 13 | // it('should set headers', () => { 14 | // let integration = { repo: '', user: '', access_token: '' }; 15 | // var Sprintly = new Sprintly(integration); 16 | // let headers = { 17 | // 'User-Agent': 'Tuddy', 18 | // 'Content-Type': 'application/json' 19 | // }; 20 | // expect(Sprintly.headers).to.eql(headers); 21 | // }); 22 | // }); 23 | // describe('#url', () => { 24 | // it('should generate an API url with querystring', () => { 25 | // let integration = { repo: 'tuddy', user: 'tudddy', access_token: 'c136002525ae2a82dec9109c304167226f8f7e7e' }; 26 | // var Sprintly = new Sprintly(integration); 27 | // var url = Sprintly.url({ per_page: 'thing' }); 28 | // expect(url).to.eql('https://api.Sprintly.com/repos/tudddy/tuddy/issues?per_page=thing'); 29 | // }); 30 | // }); 31 | describe('#push', () => { 32 | it('should be able to push stories', (done) => { 33 | 34 | var sprintly = new Sprintly({ 35 | product: '33921', 36 | email: 'jeff+tuddy@loiselles.com', 37 | key: 'WHCRvxdjE6aptnBfjZ83ZWaVQEjn7GeF' 38 | }); 39 | 40 | let stories = [ 41 | { 42 | type: 'task', 43 | name: 'test3' 44 | }, 45 | { 46 | type: 'task', 47 | name: 'test4' 48 | } 49 | ]; 50 | 51 | sprintly.push(stories).then((stories) => { 52 | expect(stories).to.have.length(2); 53 | done(); 54 | }) 55 | }); 56 | }); 57 | describe('#pull', () => { 58 | it('should be able to pull stories', (done) => { 59 | var sprintly = new Sprintly({ 60 | product: '33921', 61 | email: 'jeff+tuddy@loiselles.com', 62 | key: 'WHCRvxdjE6aptnBfjZ83ZWaVQEjn7GeF' 63 | }); 64 | sprintly.pull().then((stories) => { 65 | expect(stories).to.have.length(1); 66 | done(); 67 | }); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /fixtures/api.github.com-443/143948298586250163: -------------------------------------------------------------------------------- 1 | POST /repos/tudddy/tuddy/issues?access_token=c136002525ae2a82dec9109c304167226f8f7e7e 2 | content-type: application/json 3 | host: api.github.com 4 | accept: application/json 5 | body: {\"title\":\"test4\"} 6 | 7 | HTTP/1.1 201 undefined 8 | server: GitHub.com 9 | date: Thu, 13 Aug 2015 16:23:05 GMT 10 | content-type: application/json; charset=utf-8 11 | content-length: 1450 12 | status: 201 Created 13 | x-ratelimit-limit: 5000 14 | x-ratelimit-remaining: 4999 15 | x-ratelimit-reset: 1439486585 16 | cache-control: private, max-age=60, s-maxage=60 17 | etag: "e9a4b02d3581b3440ea28f2f010c02ea" 18 | x-oauth-scopes: gist, repo, user 19 | x-accepted-oauth-scopes: 20 | location: https://api.github.com/repos/tudddy/tuddy/issues/3 21 | vary: Accept, Authorization, Cookie, X-GitHub-OTP 22 | x-github-media-type: github.v3 23 | x-xss-protection: 1; mode=block 24 | x-frame-options: deny 25 | content-security-policy: default-src 'none' 26 | access-control-allow-credentials: true 27 | access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 28 | access-control-allow-origin: * 29 | x-github-request-id: 183D2EF0:1CB7:2496D90:55CCC469 30 | strict-transport-security: max-age=31536000; includeSubdomains; preload 31 | x-content-type-options: nosniff 32 | x-served-by: 2d7a5e35115884240089368322196939 33 | 34 | {"url":"https://api.github.com/repos/tudddy/tuddy/issues/3","labels_url":"https://api.github.com/repos/tudddy/tuddy/issues/3/labels{/name}","comments_url":"https://api.github.com/repos/tudddy/tuddy/issues/3/comments","events_url":"https://api.github.com/repos/tudddy/tuddy/issues/3/events","html_url":"https://github.com/tudddy/tuddy/issues/3","id":100812366,"number":3,"title":"test4","user":{"login":"tudddy","id":13784755,"avatar_url":"https://avatars.githubusercontent.com/u/13784755?v=3","gravatar_id":"","url":"https://api.github.com/users/tudddy","html_url":"https://github.com/tudddy","followers_url":"https://api.github.com/users/tudddy/followers","following_url":"https://api.github.com/users/tudddy/following{/other_user}","gists_url":"https://api.github.com/users/tudddy/gists{/gist_id}","starred_url":"https://api.github.com/users/tudddy/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tudddy/subscriptions","organizations_url":"https://api.github.com/users/tudddy/orgs","repos_url":"https://api.github.com/users/tudddy/repos","events_url":"https://api.github.com/users/tudddy/events{/privacy}","received_events_url":"https://api.github.com/users/tudddy/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":0,"created_at":"2015-08-13T16:23:05Z","updated_at":"2015-08-13T16:23:05Z","closed_at":null,"body":null,"closed_by":null} -------------------------------------------------------------------------------- /fixtures/api.github.com-443/14394829859068991: -------------------------------------------------------------------------------- 1 | POST /repos/tudddy/tuddy/issues?access_token=c136002525ae2a82dec9109c304167226f8f7e7e 2 | content-type: application/json 3 | host: api.github.com 4 | accept: application/json 5 | body: {\"title\":\"test3\"} 6 | 7 | HTTP/1.1 201 undefined 8 | server: GitHub.com 9 | date: Thu, 13 Aug 2015 16:23:05 GMT 10 | content-type: application/json; charset=utf-8 11 | content-length: 1450 12 | status: 201 Created 13 | x-ratelimit-limit: 5000 14 | x-ratelimit-remaining: 4998 15 | x-ratelimit-reset: 1439486585 16 | cache-control: private, max-age=60, s-maxage=60 17 | etag: "ad63259950d92753ca44b79670fe5388" 18 | x-oauth-scopes: gist, repo, user 19 | x-accepted-oauth-scopes: 20 | location: https://api.github.com/repos/tudddy/tuddy/issues/4 21 | vary: Accept, Authorization, Cookie, X-GitHub-OTP 22 | x-github-media-type: github.v3 23 | x-xss-protection: 1; mode=block 24 | x-frame-options: deny 25 | content-security-policy: default-src 'none' 26 | access-control-allow-credentials: true 27 | access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 28 | access-control-allow-origin: * 29 | x-github-request-id: 183D2EF0:1CB0:64E9B7:55CCC469 30 | strict-transport-security: max-age=31536000; includeSubdomains; preload 31 | x-content-type-options: nosniff 32 | x-served-by: cee4c0729c8e9147e7abcb45b9d69689 33 | 34 | {"url":"https://api.github.com/repos/tudddy/tuddy/issues/4","labels_url":"https://api.github.com/repos/tudddy/tuddy/issues/4/labels{/name}","comments_url":"https://api.github.com/repos/tudddy/tuddy/issues/4/comments","events_url":"https://api.github.com/repos/tudddy/tuddy/issues/4/events","html_url":"https://github.com/tudddy/tuddy/issues/4","id":100812367,"number":4,"title":"test3","user":{"login":"tudddy","id":13784755,"avatar_url":"https://avatars.githubusercontent.com/u/13784755?v=3","gravatar_id":"","url":"https://api.github.com/users/tudddy","html_url":"https://github.com/tudddy","followers_url":"https://api.github.com/users/tudddy/followers","following_url":"https://api.github.com/users/tudddy/following{/other_user}","gists_url":"https://api.github.com/users/tudddy/gists{/gist_id}","starred_url":"https://api.github.com/users/tudddy/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tudddy/subscriptions","organizations_url":"https://api.github.com/users/tudddy/orgs","repos_url":"https://api.github.com/users/tudddy/repos","events_url":"https://api.github.com/users/tudddy/events{/privacy}","received_events_url":"https://api.github.com/users/tudddy/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":0,"created_at":"2015-08-13T16:23:05Z","updated_at":"2015-08-13T16:23:05Z","closed_at":null,"body":null,"closed_by":null} -------------------------------------------------------------------------------- /lib/plugins/jira.js: -------------------------------------------------------------------------------- 1 | var SCSF = require('../scsf'); 2 | var JiraClient = require('jira-connector'); 3 | 4 | class Jira { 5 | constructor(integration) { 6 | if (!('host' in integration)) { 7 | throw new Error('missing integration.host'); 8 | } 9 | if (!('username' in integration)) { 10 | throw new Error('missing integration.username'); 11 | } 12 | if (!('password' in integration)) { 13 | throw new Error('missing integration.password'); 14 | } 15 | this.integration = integration; 16 | this.headers = { 17 | 18 | } 19 | this.client = new JiraClient({ 20 | host: this.integration.host, 21 | basic_auth: { 22 | username: this.integration.username, 23 | password: this.integration.password 24 | } 25 | }); 26 | } 27 | push(stories) { 28 | return Promise.all(stories.map((story) => { 29 | return new Promise((resolve, reject) => { 30 | this.client.issue.createIssue({ fields: Jira.toJira(story) }, function(err, issue) { 31 | if (err) { 32 | return reject(err); 33 | } 34 | return resolve({ 35 | id: issue.id, 36 | key: issue.key, 37 | url: issue.url 38 | }); 39 | }); 40 | }); 41 | })); 42 | } 43 | /** 44 | * Retrieve stories from remote integration 45 | * @return {array} Stories retrieved from remote integration 46 | */ 47 | pull() { 48 | return new Promise((resolve, reject) => { 49 | this.client.search.search({ 50 | }, function(err, result) { 51 | if (err) { 52 | return reject(err); 53 | } 54 | return resolve(result.issues.map(Jira.toSCSF)); 55 | }); 56 | }); 57 | } 58 | static toJira(input) { 59 | var merged = SCSF.merge({ data: input }); 60 | var data = merged.data; 61 | return { 62 | id: data.id, 63 | self: data.self, 64 | key: data.key, 65 | created: data.date.created, 66 | updated: data.date.updated, 67 | duedate: data.date.due, 68 | summary: data.name, 69 | description: data.description, 70 | project: { 71 | id: data.project.id 72 | }, 73 | issuetype: { 74 | name: data.type 75 | } 76 | } 77 | } 78 | static toSCSF(data) { 79 | var jira = { 80 | meta: { 81 | source: { 82 | name: 'jira', 83 | data: data 84 | } 85 | }, 86 | data: { 87 | id: data.id, 88 | self: data.self, 89 | key: data.key, 90 | name: data.fields.summary, 91 | description: data.fields.description, 92 | type: data.fields.issuetype.name, 93 | date: { 94 | due: data.fields.duedate, 95 | created: data.fields.created, 96 | updated: data.fields.updated 97 | }, 98 | project: { 99 | id: data.fields.project.id 100 | } 101 | } 102 | } 103 | return SCSF.merge(jira); 104 | } 105 | } 106 | 107 | module.exports = Jira; 108 | -------------------------------------------------------------------------------- /dist/tuddy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Function that returns an instance of client 3 | * @return {instance} Instance of client 4 | */ 5 | "use strict"; 6 | 7 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 8 | 9 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 10 | 11 | function tuddy() { 12 | return new Client(); 13 | } 14 | 15 | /** 16 | * Manages pushing, pulling, and syncing from multiple integrations 17 | */ 18 | 19 | var Client = (function () { 20 | function Client() { 21 | _classCallCheck(this, Client); 22 | 23 | this.plugins = new Map(); 24 | } 25 | 26 | /** 27 | * Adds an integration by adding an instantiated plugin to the Map 28 | * @param {object} integration Integration name, type, and authentication info 29 | */ 30 | 31 | _createClass(Client, [{ 32 | key: "addIntegration", 33 | value: function addIntegration(integration) { 34 | try { 35 | var plugin = require("./plugins/" + integration.type); 36 | } catch (err) { 37 | throw new Error(integration.type + " is not a valid plugin"); 38 | } 39 | this.plugins.set(integration.name, new plugin(integration)); 40 | } 41 | 42 | /** 43 | * Returns an integration by name, throwing an error if it doesn't exist 44 | * @param {string} integrationName Integration identifier 45 | * @return {instance} Instance of plugin 46 | */ 47 | }, { 48 | key: "getIntegration", 49 | value: function getIntegration(integrationName) { 50 | if (!this.plugins.has(integrationName)) { 51 | throw new Error("\"" + integrationName + "\" is not a registered integration"); 52 | } 53 | return this.plugins.get(integrationName); 54 | } 55 | 56 | /** 57 | * Retrieve all stories from Integration 58 | * @param {string} integrationName Integration identifier 59 | * @return {instance} Instance of plugin 60 | */ 61 | }, { 62 | key: "pull", 63 | value: function pull(integrationName) { 64 | var instance = this.getIntegration(integrationName); 65 | return instance.pull(); 66 | } 67 | 68 | /** 69 | * Creates all stories on integration 70 | * @param {string} integrationName Integration identifier 71 | * @param {array} stories Array of stories 72 | * @return {instance} Instance of plugin 73 | */ 74 | }, { 75 | key: "push", 76 | value: function push(integrationName, stories) { 77 | var instance = this.getIntegration(integrationName); 78 | return instance.push(stories); 79 | } 80 | }]); 81 | 82 | return Client; 83 | })(); 84 | 85 | module.exports = tuddy; -------------------------------------------------------------------------------- /fixtures/api.trello.com-443/143948361720759024: -------------------------------------------------------------------------------- 1 | GET /1/boards/55ccc49df22b549c64f3b91e/cards/all?key=87105c2d4268dd05af77c83b64326f8a&token=08ba7b31356c8f61ffc9eeebe4e7fa1e6c02ca722221b20175d35253d6dd7f04 2 | host: api.trello.com 3 | accept: application/json 4 | content-type: application/json 5 | body: {\"key\":\"87105c2d4268dd05af77c83b64326f8a\",\"token\":\"08ba7b31356c8f61ffc9eeebe4e7fa1e6c02ca722221b20175d35253d6dd7f04\"} 6 | 7 | HTTP/1.1 200 undefined 8 | cache-control: max-age=0, must-revalidate, no-cache, no-store 9 | x-content-type-options: nosniff 10 | strict-transport-security: max-age=15768000 11 | x-xss-protection: 1; mode=block 12 | x-frame-options: DENY 13 | x-trello-version: 1.452.0 14 | x-trello-environment: Production 15 | set-cookie: dsc=2d01953d8c15cdf28296daa66043254a57c4eda4efbc08a448287bf464aef589; Path=/; Expires=Sun, 16 Aug 2015 16:33:36 GMT; Secure 16 | set-cookie: visid_incap_168562=dtc4QKmQRjGPj+lapF0hI+DGzFUAAAAAQUIPAAAAAABCXk0fb4nuHk9n+BKyhtwz; expires=Sat, 12 Aug 2017 09:23:05 GMT; path=/; Domain=.trello.com 17 | set-cookie: incap_ses_222_168562=pndGCgdafFjY2KrYg7QUA+DGzFUAAAAAzAsbye27816cL5XtLeYrqA==; path=/; Domain=.trello.com 18 | access-control-allow-origin: * 19 | access-control-allow-methods: GET, PUT, POST, DELETE 20 | x-server-time: 1439483616938 21 | expires: Thu, 01 Jan 1970 00:00:00 22 | content-type: application/json; charset=utf-8 23 | content-length: 1668 24 | etag: W/"ctu1y7R7axsuZrSn78YOLg==" 25 | vary: Accept-Encoding 26 | date: Thu, 13 Aug 2015 16:33:36 GMT 27 | x-iinfo: 10-8436195-8397110 PNNN RT(1439483615555 126) q(0 0 0 -1) r(4 4) U13 28 | x-cdn: Incapsula 29 | 30 | [{"id":"55ccc4a9d3294f7a68cc90f3","checkItemStates":[],"closed":false,"dateLastActivity":"2015-08-13T16:24:09.303Z","desc":"","descData":null,"idBoard":"55ccc49df22b549c64f3b91e","idList":"55ccc4a5e08262688ef56a96","idMembersVoted":[],"idShort":1,"idAttachmentCover":null,"manualCoverAttachment":false,"idLabels":[],"name":"test1","pos":65535,"shortLink":"ZNUzB6mn","badges":{"votes":0,"viewingMemberVoted":false,"subscribed":false,"fogbugz":"","checkItems":0,"checkItemsChecked":0,"comments":0,"attachments":0,"description":false,"due":null},"due":null,"email":"tuddy2+55ccc162f6f892ec7d80b27f+55ccc4a9d3294f7a68cc90f3+8ce2ddff99f7a94cb079f13144069242ecc8ed54@boards.trello.com","idChecklists":[],"idMembers":[],"labels":[],"shortUrl":"https://trello.com/c/ZNUzB6mn","subscribed":false,"url":"https://trello.com/c/ZNUzB6mn/1-test1"},{"id":"55ccc4aa7550602f3d6b0571","checkItemStates":[],"closed":false,"dateLastActivity":"2015-08-13T16:24:10.859Z","desc":"","descData":null,"idBoard":"55ccc49df22b549c64f3b91e","idList":"55ccc4a5e08262688ef56a96","idMembersVoted":[],"idShort":2,"idAttachmentCover":null,"manualCoverAttachment":false,"idLabels":[],"name":"test2","pos":131071,"shortLink":"hJOLWPMO","badges":{"votes":0,"viewingMemberVoted":false,"subscribed":false,"fogbugz":"","checkItems":0,"checkItemsChecked":0,"comments":0,"attachments":0,"description":false,"due":null},"due":null,"email":"tuddy2+55ccc162f6f892ec7d80b27f+55ccc4aa7550602f3d6b0571+2085ebf29bd3de1102493fae4191a62da16a4b44@boards.trello.com","idChecklists":[],"idMembers":[],"labels":[],"shortUrl":"https://trello.com/c/hJOLWPMO","subscribed":false,"url":"https://trello.com/c/hJOLWPMO/2-test2"}] -------------------------------------------------------------------------------- /fixtures/www.pivotaltracker.com-443/1439515615240831: -------------------------------------------------------------------------------- 1 | GET /services/v5/projects/1407870/stories 2 | x-trackertoken: 690eb36760eff666af11c563510fc616 3 | content-type: application/json 4 | host: www.pivotaltracker.com 5 | accept: application/json 6 | 7 | HTTP/1.1 200 undefined 8 | content-type: application/json; charset=utf-8 9 | transfer-encoding: chunked 10 | status: 200 OK 11 | x-tracker-project-version: 8 12 | x-tracker-pagination-total: 7 13 | x-tracker-pagination-offset: 0 14 | x-tracker-pagination-limit: 100 15 | x-tracker-pagination-returned: 7 16 | x-ua-compatible: IE=Edge,chrome=1 17 | etag: "ab2970f033bca56de0866ecedfd8e45f" 18 | cache-control: max-age=0, private, must-revalidate 19 | x-request-id: d9c7e8432d92e29d1c291e86d8cbe74b 20 | x-runtime: 0.052035 21 | date: Fri, 14 Aug 2015 01:26:55 GMT 22 | x-rack-cache: miss 23 | x-powered-by: Phusion Passenger 4.0.41 24 | server: nginx/1.6.0 + Phusion Passenger 4.0.41 25 | access-control-allow-origin: * 26 | access-control-allow-credentials: false 27 | access-control-allow-methods: GET, POST, PUT, DELETE, OPTIONS 28 | access-control-allow-headers: X-TrackerToken,DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,X-Tracker-Warn-Unless-Project-Version-Is 29 | x-tracker-client-pinger-interval: 12 30 | 31 | [{"kind":"story","id":101250808,"created_at":"2015-08-13T23:18:17Z","updated_at":"2015-08-13T23:18:17Z","story_type":"feature","name":"test2","current_state":"unscheduled","requested_by_id":1762546,"project_id":1407870,"url":"https://www.pivotaltracker.com/story/show/101250808","owner_ids":[],"labels":[]},{"kind":"story","id":101250806,"created_at":"2015-08-13T23:18:17Z","updated_at":"2015-08-13T23:18:17Z","story_type":"feature","name":"test1","current_state":"unscheduled","requested_by_id":1762546,"project_id":1407870,"url":"https://www.pivotaltracker.com/story/show/101250806","owner_ids":[],"labels":[]},{"kind":"story","id":101250268,"created_at":"2015-08-13T23:07:42Z","updated_at":"2015-08-13T23:07:42Z","story_type":"feature","name":"test1","current_state":"unscheduled","requested_by_id":1762546,"project_id":1407870,"url":"https://www.pivotaltracker.com/story/show/101250268","owner_ids":[],"labels":[]},{"kind":"story","id":101250266,"created_at":"2015-08-13T23:07:41Z","updated_at":"2015-08-13T23:07:41Z","story_type":"feature","name":"test2","current_state":"unscheduled","requested_by_id":1762546,"project_id":1407870,"url":"https://www.pivotaltracker.com/story/show/101250266","owner_ids":[],"labels":[]},{"kind":"story","id":101250140,"created_at":"2015-08-13T23:04:06Z","updated_at":"2015-08-13T23:04:06Z","story_type":"feature","name":"test1","current_state":"unscheduled","requested_by_id":1762546,"project_id":1407870,"url":"https://www.pivotaltracker.com/story/show/101250140","owner_ids":[],"labels":[]},{"kind":"story","id":101249982,"created_at":"2015-08-13T23:00:41Z","updated_at":"2015-08-13T23:00:41Z","story_type":"feature","name":"test1","current_state":"unscheduled","requested_by_id":1762546,"project_id":1407870,"url":"https://www.pivotaltracker.com/story/show/101249982","owner_ids":[],"labels":[]},{"kind":"story","id":101249980,"created_at":"2015-08-13T23:00:41Z","updated_at":"2015-08-13T23:00:41Z","story_type":"feature","name":"test2","current_state":"unscheduled","requested_by_id":1762546,"project_id":1407870,"url":"https://www.pivotaltracker.com/story/show/101249980","owner_ids":[],"labels":[]}] -------------------------------------------------------------------------------- /fixtures/teuxdeux.com-443/144344327408858641: -------------------------------------------------------------------------------- 1 | GET /api/v1/todos/calendar 2 | x-csrf-token: d151f2a1c31d5b5374f64112be07c3cd637b9f8b6450fa54ee3dd492948eed0c 3 | host: teuxdeux.com 4 | accept: application/json 5 | 6 | HTTP/1.1 200 OK 7 | content-type: application/json; charset=UTF-8 8 | date: Mon, 28 Sep 2015 12:27:54 GMT 9 | server: nginx/1.2.1 10 | set-cookie: rack.session=BAh7CUkiD3Nlc3Npb25faWQGOgZFRiJFNTA4YWZiZDdlZTdjOTViOWQ2YThi%0AZDg0ZDI5ODI1MmFlYThmNzMwNjcwMjdmMThiMWRlYTgyYTg1NDI5ZmNlNEki%0ADl9fRkxBU0hfXwY7AEZ7AEkiCWNzcmYGOwBGIkVkMTUxZjJhMWMzMWQ1YjUz%0ANzRmNjQxMTJiZTA3YzNjZDYzN2I5ZjhiNjQ1MGZhNTRlZTNkZDQ5Mjk0OGVl%0AZDBjSSIMdXNlcl9pZAY7AEZpAizW%0A--fae485c6811e7ef3d7c07d25450f0f0e814f5570; path=/; expires=Wed, 28 Oct 2015 12:27:54 -0000; secure; HttpOnly 11 | status: 200 OK 12 | x-frame-options: sameorigin, SAMEORIGIN 13 | x-xss-protection: 1; mode=block 14 | content-length: 2795 15 | connection: Close 16 | 17 | [{"id":34457207,"cid":null,"uuid":"d073f875-d904-4443-bc4f-2b6a92371510","text":"test2","position":1,"list_id":null,"start_date":"2015-09-11","current_date":"2015-09-28","done":false,"user_id":54828,"recurring_todo_id":null,"recurring":false,"created_at":"2015-09-11T15:04:41+00:00","updated_at":"2015-09-11T15:04:41+00:00","text_updated_at":null,"done_updated_at":null,"position_updated_at":null,"column_updated_at":null,"deleted_at":null,"recurring_todo":null},{"id":34457358,"cid":null,"uuid":"e9a1dbbc-0ffe-4679-88e2-60cdde25dfe1","text":"test4","position":2,"list_id":null,"start_date":"2015-09-11","current_date":"2015-09-28","done":false,"user_id":54828,"recurring_todo_id":null,"recurring":false,"created_at":"2015-09-11T15:11:39+00:00","updated_at":"2015-09-11T15:11:39+00:00","text_updated_at":null,"done_updated_at":null,"position_updated_at":null,"column_updated_at":null,"deleted_at":null,"recurring_todo":null},{"id":34457359,"cid":null,"uuid":"3f0275e6-7d2e-49a6-9991-e30f23ecf0c6","text":"test3","position":3,"list_id":null,"start_date":"2015-09-11","current_date":"2015-09-28","done":false,"user_id":54828,"recurring_todo_id":null,"recurring":false,"created_at":"2015-09-11T15:11:39+00:00","updated_at":"2015-09-11T15:11:39+00:00","text_updated_at":null,"done_updated_at":null,"position_updated_at":null,"column_updated_at":null,"deleted_at":null,"recurring_todo":null},{"id":34800618,"cid":null,"uuid":"3ef6698d-2d53-4d4d-b11c-8066d1635320","text":"test3","position":4,"list_id":null,"start_date":"2015-09-11","current_date":"2015-09-28","done":false,"user_id":54828,"recurring_todo_id":null,"recurring":false,"created_at":"2015-09-28T12:27:53+00:00","updated_at":"2015-09-28T12:27:53+00:00","text_updated_at":null,"done_updated_at":null,"position_updated_at":null,"column_updated_at":null,"deleted_at":null,"recurring_todo":null},{"id":34800619,"cid":null,"uuid":"a14aec8b-6c5f-475d-824a-1209223bce69","text":"test4","position":5,"list_id":null,"start_date":"2015-09-11","current_date":"2015-09-28","done":false,"user_id":54828,"recurring_todo_id":null,"recurring":false,"created_at":"2015-09-28T12:27:53+00:00","updated_at":"2015-09-28T12:27:53+00:00","text_updated_at":null,"done_updated_at":null,"position_updated_at":null,"column_updated_at":null,"deleted_at":null,"recurring_todo":null},{"id":34457206,"cid":null,"uuid":"9134d545-7c9d-42ae-a9e3-88b5d14b528e","text":"test1","position":6,"list_id":null,"start_date":"2015-09-11","current_date":"2015-09-28","done":true,"user_id":54828,"recurring_todo_id":null,"recurring":false,"created_at":"2015-09-11T15:04:40+00:00","updated_at":"2015-09-28T12:27:33+00:00","text_updated_at":null,"done_updated_at":"2015-09-28T12:27:33+00:00","position_updated_at":null,"column_updated_at":null,"deleted_at":null,"recurring_todo":null}] -------------------------------------------------------------------------------- /lib/plugins/trello.js: -------------------------------------------------------------------------------- 1 | var SCSF = require('../scsf'); 2 | var trello = require('node-trello'); 3 | 4 | class Trello { 5 | constructor(integration) { 6 | if (!('key' in integration)) { 7 | throw new Error('missing integration.key'); 8 | } 9 | if (!('token' in integration)) { 10 | throw new Error('missing integration.token'); 11 | } 12 | if (!('board_id' in integration)) { 13 | throw new Error('missing integration.board_id'); 14 | } 15 | this.integration = integration; 16 | this.headers = { 17 | 'User-Agent': 'Tuddy', 18 | 'Content-Type': 'application/json' 19 | }; 20 | } 21 | /** 22 | * Pulls card data from Trello 23 | */ 24 | pull() { 25 | return new Promise((resolve, reject) => { 26 | let stories = []; 27 | var t = new trello(this.integration.key, this.integration.token); 28 | t.get(`/1/boards/${this.integration.board_id}/cards/all`, function(err, data){ 29 | if (err) { 30 | return reject(err); 31 | } 32 | data.forEach((card) => { 33 | stories.push(Trello.toSCSF(card)); 34 | }); 35 | return resolve(stories); 36 | }); 37 | }); 38 | } 39 | /** 40 | * Pushes cad data to toTrello 41 | * 42 | * @param stories 43 | */ 44 | push(stories) { 45 | return Promise.all(stories.map((story) => { 46 | var t = new trello(this.integration.key, this.integration.token); 47 | var data = Trello.toTrello(story); 48 | data.idList = this.integration.idList; 49 | return new Promise((resolve, reject) => { 50 | return t.post('/1/cards', data, (err, body) => { 51 | if (err) { 52 | return reject(err); 53 | } 54 | return resolve(Trello.toSCSF(body)); 55 | }); 56 | }); 57 | })); 58 | } 59 | /** 60 | * Convert SCSF data to Trello format 61 | */ 62 | static toTrello(input) { 63 | var merged = SCSF.merge({ data: input }); 64 | var data = merged.data; 65 | return { 66 | name: data.name, 67 | shortLink: data.key, 68 | url: data.url, 69 | desc: data.description, 70 | shortUrl: data.short_url, 71 | email: data.email, 72 | due: data.date.due, 73 | dateLastActivity: data.date.updated, 74 | closed: (function(data){ 75 | if (data.status == 'complete') { 76 | return true; 77 | } else { 78 | return false; 79 | } 80 | })(data) 81 | } 82 | } 83 | /** 84 | * Converts Trello data to SCSF format 85 | */ 86 | static toSCSF(data) { 87 | var trello = { 88 | meta: { 89 | source: { 90 | name: 'trello', 91 | data: data 92 | } 93 | }, 94 | data: { 95 | id: data.id, 96 | key: data.shortLink, 97 | url: data.url, 98 | name: data.name, 99 | description: data.desc, 100 | short_url: data.shortUrl, 101 | email: data.email, 102 | date: { 103 | due: data.due, 104 | updated: data.dateLastActivity 105 | }, 106 | status: (function(data) { 107 | switch (data.closed) { 108 | case false: 109 | return 'incomplete'; 110 | break; 111 | case true: 112 | return 'complete'; 113 | break; 114 | } 115 | })(data) 116 | } 117 | } 118 | return SCSF.merge(trello); 119 | } 120 | } 121 | 122 | module.exports = Trello 123 | -------------------------------------------------------------------------------- /dist/plugins/jira.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 6 | 7 | var SCSF = require('../scsf'); 8 | var JiraClient = require('jira-connector'); 9 | 10 | var Jira = (function () { 11 | function Jira(integration) { 12 | _classCallCheck(this, Jira); 13 | 14 | if (!('host' in integration)) { 15 | throw new Error('missing integration.host'); 16 | } 17 | if (!('username' in integration)) { 18 | throw new Error('missing integration.username'); 19 | } 20 | if (!('password' in integration)) { 21 | throw new Error('missing integration.password'); 22 | } 23 | this.integration = integration; 24 | this.headers = {}; 25 | this.client = new JiraClient({ 26 | host: this.integration.host, 27 | basic_auth: { 28 | username: this.integration.username, 29 | password: this.integration.password 30 | } 31 | }); 32 | } 33 | 34 | _createClass(Jira, [{ 35 | key: 'push', 36 | value: function push(stories) { 37 | var _this = this; 38 | 39 | return Promise.all(stories.map(function (story) { 40 | return new Promise(function (resolve, reject) { 41 | _this.client.issue.createIssue({ fields: Jira.toJira(story) }, function (err, issue) { 42 | if (err) { 43 | return reject(err); 44 | } 45 | return resolve({ 46 | id: issue.id, 47 | key: issue.key, 48 | url: issue.url 49 | }); 50 | }); 51 | }); 52 | })); 53 | } 54 | 55 | /** 56 | * Retrieve stories from remote integration 57 | * @return {array} Stories retrieved from remote integration 58 | */ 59 | }, { 60 | key: 'pull', 61 | value: function pull() { 62 | var _this2 = this; 63 | 64 | return new Promise(function (resolve, reject) { 65 | _this2.client.search.search({}, function (err, result) { 66 | if (err) { 67 | return reject(err); 68 | } 69 | return resolve(result.issues.map(Jira.toSCSF)); 70 | }); 71 | }); 72 | } 73 | }], [{ 74 | key: 'toJira', 75 | value: function toJira(input) { 76 | var merged = SCSF.merge({ data: input }); 77 | var data = merged.data; 78 | return { 79 | id: data.id, 80 | self: data.self, 81 | key: data.key, 82 | created: data.date.created, 83 | updated: data.date.updated, 84 | duedate: data.date.due, 85 | summary: data.name, 86 | description: data.description, 87 | project: { 88 | id: data.project.id 89 | }, 90 | issuetype: { 91 | name: data.type 92 | } 93 | }; 94 | } 95 | }, { 96 | key: 'toSCSF', 97 | value: function toSCSF(data) { 98 | var jira = { 99 | meta: { 100 | source: { 101 | name: 'jira', 102 | data: data 103 | } 104 | }, 105 | data: { 106 | id: data.id, 107 | self: data.self, 108 | key: data.key, 109 | name: data.fields.summary, 110 | description: data.fields.description, 111 | type: data.fields.issuetype.name, 112 | date: { 113 | due: data.fields.duedate, 114 | created: data.fields.created, 115 | updated: data.fields.updated 116 | }, 117 | project: { 118 | id: data.fields.project.id 119 | } 120 | } 121 | }; 122 | return SCSF.merge(jira); 123 | } 124 | }]); 125 | 126 | return Jira; 127 | })(); 128 | 129 | module.exports = Jira; -------------------------------------------------------------------------------- /test/pivotal.spec.js: -------------------------------------------------------------------------------- 1 | require('replay'); 2 | var expect = require('chai').expect; 3 | 4 | var Pivotal = require('../lib/plugins/pivotal.js'); 5 | 6 | describe('Pivotal', () => { 7 | describe('#constructor', () => { 8 | 9 | }); 10 | describe('#toSCSF', () => { 11 | it('should correctly convert format', () => { 12 | var pivotal = { 13 | kind: 'story', 14 | id: 101250808, 15 | created_at: '2015-08-13T23:18:17Z', 16 | updated_at: '2015-08-13T23:18:17Z', 17 | story_type: 'feature', 18 | name: 'test2', 19 | current_state: 'unscheduled', 20 | requested_by_id: 1762546, 21 | project_id: 1407870, 22 | url: 'https://www.pivotaltracker.com/story/show/101250808', 23 | owner_ids: [], 24 | labels: [] 25 | }; 26 | 27 | var expected = { 28 | meta: { 29 | source: { 30 | id: null, 31 | name: 'pivotal', 32 | key: null 33 | } 34 | }, 35 | data: { 36 | id: 101250808, 37 | self: null, 38 | key: null, 39 | name: 'test2', 40 | description: null, 41 | type: 'feature', 42 | url: 'https://www.pivotaltracker.com/story/show/101250808', 43 | archived: null, 44 | status: 'incomplete', 45 | email: null, 46 | short_url: null, 47 | date: { 48 | start: null, 49 | end: null, 50 | due: null, 51 | created: '2015-08-13T23:18:17Z', 52 | updated: '2015-08-13T23:18:17Z', 53 | completed: null, 54 | deleted: null 55 | }, 56 | lists: [], 57 | labels: [], 58 | project: { 59 | id: 1407870 60 | }, 61 | users: [] 62 | } 63 | }; 64 | 65 | var story = Pivotal.toSCSF(pivotal); 66 | expect(story.meta.source.data).to.be.an('object'); 67 | delete story.meta.source.data; 68 | expect(story).to.eql(expected); 69 | }); 70 | }); 71 | describe('#toPivotal', () => { 72 | it('should correctly convert format', () => { 73 | var scsf = { 74 | id: 101250808, 75 | self: null, 76 | key: null, 77 | name: 'test2', 78 | description: null, 79 | type: 'feature', 80 | url: 'https://www.pivotaltracker.com/story/show/101250808', 81 | archived: null, 82 | status: 'incomplete', 83 | email: null, 84 | short_url: null, 85 | date: { 86 | start: null, 87 | end: null, 88 | due: null, 89 | created: '2015-08-13T23:18:17Z', 90 | updated: '2015-08-13T23:18:17Z', 91 | completed: null, 92 | deleted: null 93 | }, 94 | lists: [], 95 | labels: [], 96 | project: { 97 | id: 1407870 98 | }, 99 | users: [] 100 | }; 101 | 102 | var pivotal = Pivotal.toPivotal(scsf); 103 | var expected = { 104 | id: 101250808, 105 | name: 'test2', 106 | description: null, 107 | kind: 'story', 108 | story_type: 'feature', 109 | created_at: '2015-08-13T23:18:17Z', 110 | updated_at: '2015-08-13T23:18:17Z', 111 | project_id: 1407870, 112 | current_state: 'unscheduled' 113 | }; 114 | expect(pivotal).to.eql(expected) 115 | }); 116 | }); 117 | describe('#push', () => { 118 | it('should push stories', (done) => { 119 | let pivotal = new Pivotal({ 120 | project: '1407870', 121 | token: '690eb36760eff666af11c563510fc616' 122 | }); 123 | let stories = [ 124 | { 125 | name: 'test1' 126 | }, 127 | { 128 | name: 'test2' 129 | } 130 | ]; 131 | pivotal.push(stories).then((stories) => { 132 | expect(stories).to.have.length(2); 133 | done(); 134 | }).catch((err) => { 135 | done(err); 136 | }); 137 | }); 138 | }); 139 | describe('#pull', () => { 140 | it('should pull stories', (done) => { 141 | let pivotal = new Pivotal({ 142 | project: '1407870', 143 | token: '690eb36760eff666af11c563510fc616' 144 | }); 145 | pivotal.pull().then((stories) => { 146 | expect(stories).to.have.length(7); 147 | done(); 148 | }).catch((err) => { 149 | done(err); 150 | }); 151 | }) 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /lib/plugins/teuxdeux.js: -------------------------------------------------------------------------------- 1 | var SCSF = require('../scsf'); 2 | var cheerio = require('cheerio'); 3 | var Promise = require('bluebird'); 4 | var request = require('request'); 5 | request.get = Promise.promisify(request.get, request); 6 | request.post = Promise.promisify(request.post, request); 7 | request.put = Promise.promisify(request.put, request); 8 | 9 | var request = request.defaults({jar: true}); 10 | 11 | class Teuxdeux { 12 | constructor(integration) { 13 | if (!('username' in integration)) { 14 | throw new Error('missing integration.user'); 15 | } 16 | if (!('password' in integration)) { 17 | throw new Error('missing integration.pass'); 18 | } 19 | this.loginOperation = null; 20 | this.integration = integration; 21 | this.headers = { 22 | 'User-Agent': 'Tuddy', 23 | 'Content-Type': 'application/json' 24 | } 25 | } 26 | /** 27 | * Private method to ensure that login occurs before another action 28 | * @return {[type]} [description] 29 | */ 30 | _login() { 31 | var self = this; 32 | if (!self.loginOperation) { 33 | return self.loginOperation = request.get({ url: 'https://teuxdeux.com/login'}).spread(function(res, body){ 34 | var $ = cheerio.load(body); 35 | self.integration.authenticity_token = $('input[name=authenticity_token]').val(); 36 | }).then(function(){ 37 | return request.post({ url: 'https://teuxdeux.com/login', form: self.integration }).spread(function(res, body){ 38 | return true; 39 | }); 40 | }); 41 | } 42 | return self.loginOperation; 43 | } 44 | /** 45 | * Retrieves stories from remote integration 46 | * @return {array} array of stories retrieved from remote integration 47 | */ 48 | pull() { 49 | return this._login().then(() => { 50 | return request.get({ 51 | url: 'https://teuxdeux.com/api/v1/todos/calendar', 52 | json: true, 53 | headers: { 'x-csrf-token': this.integration.authenticity_token } 54 | }).spread((res, body) => { 55 | var stories = []; 56 | body.forEach((story) => { 57 | stories.push(Teuxdeux.toSCSF(story)); 58 | }); 59 | return stories; 60 | }); 61 | }); 62 | } 63 | /** 64 | * Creates stories at remote integration 65 | * @param {array} stories array of stories to create 66 | * @return {array} array of stories that were created 67 | */ 68 | push(stories) { 69 | var out = []; 70 | return this._login().then(() => { 71 | return Promise.all(stories.map((story) => { 72 | var data = Teuxdeux.toTeuxdeux(story); 73 | return request.post({ 74 | url: 'https://teuxdeux.com/api/v1/todos', 75 | form: data, 76 | json: true, 77 | headers: { 'x-csrf-token': this.integration.authenticity_token } 78 | }).spread((res, body) => { 79 | out.push(Teuxdeux.toSCSF(body)); 80 | }); 81 | })).then(() => { 82 | return out; 83 | }); 84 | }); 85 | } 86 | /** 87 | * Converts SCSF to data to source format 88 | * @param {object} data SCSF data 89 | * @return {object} source data 90 | */ 91 | static toTeuxdeux(input) { 92 | var merged = SCSF.merge({ data: input }); 93 | var data = merged.data; 94 | return { 95 | id: data.id, 96 | text: data.name, 97 | uuid: data.key, 98 | done: (function(data){ 99 | if (data.status == 'complete') { 100 | return true; 101 | } else { 102 | return false; 103 | } 104 | })(data), 105 | current_date: data.date.start || new Date(), 106 | created_at: data.date.created, 107 | updated_at: data.date.updated 108 | } 109 | } 110 | /** 111 | * Converts source data to SCSF format 112 | * @param {object} data source data 113 | * @return {object} transformed SCSF data 114 | */ 115 | static toSCSF(data) { 116 | var teuxdeux = { 117 | meta: { 118 | source: { 119 | name: 'teuxdeux', 120 | data: data 121 | } 122 | }, 123 | data: { 124 | id: data.id, 125 | key: data.uuid, 126 | name: data.text, 127 | status: (function(data){ 128 | if (data.done == true) { 129 | return 'complete'; 130 | } else { 131 | return 'incomplete'; 132 | } 133 | })(data), 134 | date: { 135 | start: data.current_date, 136 | created: data.created_at, 137 | updated: data.updated_at 138 | } 139 | } 140 | } 141 | return SCSF.merge(teuxdeux); 142 | } 143 | } 144 | 145 | module.exports = Teuxdeux; 146 | -------------------------------------------------------------------------------- /fixtures/api.trello.com-443/143948377065823795: -------------------------------------------------------------------------------- 1 | GET /1/boards/55ccc49df22b549c64f3b91e/cards/all?key=87105c2d4268dd05af77c83b64326f8a&token=e6691ee3114053746f53504df00a736407b985ce3cd0b2957012fa702ce83065 2 | host: api.trello.com 3 | accept: application/json 4 | content-type: application/json 5 | body: {\"key\":\"87105c2d4268dd05af77c83b64326f8a\",\"token\":\"e6691ee3114053746f53504df00a736407b985ce3cd0b2957012fa702ce83065\"} 6 | 7 | HTTP/1.1 200 undefined 8 | cache-control: max-age=0, must-revalidate, no-cache, no-store 9 | x-content-type-options: nosniff 10 | strict-transport-security: max-age=15768000 11 | x-xss-protection: 1; mode=block 12 | x-frame-options: DENY 13 | x-trello-version: 1.452.0 14 | x-trello-environment: Production 15 | set-cookie: dsc=aac6773ecb83ae3703ff30e9d5ca899cf28029c02bcfdcb205a144292e57e267; Path=/; Expires=Sun, 16 Aug 2015 16:36:10 GMT; Secure 16 | set-cookie: visid_incap_168562=4N7pAMvARRSq6aAdcNZycHnHzFUAAAAAQUIPAAAAAAAvP8hHKw8k8R/J43fkWELl; expires=Sat, 12 Aug 2017 09:23:05 GMT; path=/; Domain=.trello.com 17 | set-cookie: incap_ses_222_168562=icjEYoFYEkJ/z6zYg7QUA3nHzFUAAAAArUTugj74ED+l0FkFWXYSrg==; path=/; Domain=.trello.com 18 | access-control-allow-origin: * 19 | access-control-allow-methods: GET, PUT, POST, DELETE 20 | x-server-time: 1439483770593 21 | expires: Thu, 01 Jan 1970 00:00:00 22 | content-type: application/json; charset=utf-8 23 | content-length: 3336 24 | etag: W/"xAJwanYhFvx9aiZ298dsyw==" 25 | vary: Accept-Encoding 26 | date: Thu, 13 Aug 2015 16:36:10 GMT 27 | x-iinfo: 10-8485625-8453154 PNNN RT(1439483769294 145) q(0 0 0 -1) r(3 3) U13 28 | x-cdn: Incapsula 29 | 30 | [{"id":"55ccc4a9d3294f7a68cc90f3","checkItemStates":[],"closed":false,"dateLastActivity":"2015-08-13T16:24:09.303Z","desc":"","descData":null,"idBoard":"55ccc49df22b549c64f3b91e","idList":"55ccc4a5e08262688ef56a96","idMembersVoted":[],"idShort":1,"idAttachmentCover":null,"manualCoverAttachment":false,"idLabels":[],"name":"test1","pos":65535,"shortLink":"ZNUzB6mn","badges":{"votes":0,"viewingMemberVoted":false,"subscribed":false,"fogbugz":"","checkItems":0,"checkItemsChecked":0,"comments":0,"attachments":0,"description":false,"due":null},"due":null,"email":"tuddy2+55ccc162f6f892ec7d80b27f+55ccc4a9d3294f7a68cc90f3+8ce2ddff99f7a94cb079f13144069242ecc8ed54@boards.trello.com","idChecklists":[],"idMembers":[],"labels":[],"shortUrl":"https://trello.com/c/ZNUzB6mn","subscribed":false,"url":"https://trello.com/c/ZNUzB6mn/1-test1"},{"id":"55ccc4aa7550602f3d6b0571","checkItemStates":[],"closed":false,"dateLastActivity":"2015-08-13T16:24:10.859Z","desc":"","descData":null,"idBoard":"55ccc49df22b549c64f3b91e","idList":"55ccc4a5e08262688ef56a96","idMembersVoted":[],"idShort":2,"idAttachmentCover":null,"manualCoverAttachment":false,"idLabels":[],"name":"test2","pos":131071,"shortLink":"hJOLWPMO","badges":{"votes":0,"viewingMemberVoted":false,"subscribed":false,"fogbugz":"","checkItems":0,"checkItemsChecked":0,"comments":0,"attachments":0,"description":false,"due":null},"due":null,"email":"tuddy2+55ccc162f6f892ec7d80b27f+55ccc4aa7550602f3d6b0571+2085ebf29bd3de1102493fae4191a62da16a4b44@boards.trello.com","idChecklists":[],"idMembers":[],"labels":[],"shortUrl":"https://trello.com/c/hJOLWPMO","subscribed":false,"url":"https://trello.com/c/hJOLWPMO/2-test2"},{"id":"55ccc778c605511b4dafd67d","checkItemStates":[],"closed":false,"dateLastActivity":"2015-08-13T16:36:08.527Z","desc":"","descData":null,"idBoard":"55ccc49df22b549c64f3b91e","idList":"55ccc4a5e08262688ef56a96","idMembersVoted":[],"idShort":3,"idAttachmentCover":null,"manualCoverAttachment":false,"idLabels":[],"name":"test1","pos":147455,"shortLink":"WoSysVwC","badges":{"votes":0,"viewingMemberVoted":false,"subscribed":false,"fogbugz":"","checkItems":0,"checkItemsChecked":0,"comments":0,"attachments":0,"description":false,"due":null},"due":null,"email":"tuddy2+55ccc162f6f892ec7d80b27f+55ccc778c605511b4dafd67d+cb542b213ba83706f4e9c8c3a691b4b43c73abec@boards.trello.com","idChecklists":[],"idMembers":[],"labels":[],"shortUrl":"https://trello.com/c/WoSysVwC","subscribed":false,"url":"https://trello.com/c/WoSysVwC/3-test1"},{"id":"55ccc778590aa7435ca2064d","checkItemStates":[],"closed":false,"dateLastActivity":"2015-08-13T16:36:08.776Z","desc":"","descData":null,"idBoard":"55ccc49df22b549c64f3b91e","idList":"55ccc4a5e08262688ef56a96","idMembersVoted":[],"idShort":4,"idAttachmentCover":null,"manualCoverAttachment":false,"idLabels":[],"name":"test2","pos":163839,"shortLink":"6kcAzvRV","badges":{"votes":0,"viewingMemberVoted":false,"subscribed":false,"fogbugz":"","checkItems":0,"checkItemsChecked":0,"comments":0,"attachments":0,"description":false,"due":null},"due":null,"email":"tuddy2+55ccc162f6f892ec7d80b27f+55ccc778590aa7435ca2064d+f0be33478614f53669fcfc4e4cc43fc9c6fe06d5@boards.trello.com","idChecklists":[],"idMembers":[],"labels":[],"shortUrl":"https://trello.com/c/6kcAzvRV","subscribed":false,"url":"https://trello.com/c/6kcAzvRV/4-test2"}] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tuddy 2 | 3 | [](https://gitter.im/specerator/tuddy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 4 | 5 | ## EXPERIMENTAL 6 | 7 | *Export, import, synchronize, stories, tasks, todos, such as Trello, Pivotal Tracker, JIRA, Github, Sprintly, Teuxdeux, etc.* 8 | 9 | ```javascript 10 | 11 | var client = tuddy(); 12 | 13 | client.addIntegration({ name: 'myTello', type: 'trello', key: '', token: '', board_id: ''}); 14 | 15 | client.addIntegration({ name: 'myGithub', type: 'github', user: '', repo: '', access_token: ''}); 16 | 17 | client.addIntegration({ name: 'myJira', type: 'jira', host: '', username: '', password: ''}); 18 | 19 | client.addIntegration({ name: 'myPivotal', type: 'pivotal', project: '', token: ''}); 20 | 21 | client.addIntegration({ name: 'myTeuxdeux', type: 'teuxdeux', username: '', password: ''}) 22 | 23 | client.addIntegration({ name: 'mySprintly', type: 'sprintly', product: '', email: '', key: ''}) 24 | ``` 25 | 26 | **If you would like to pull data from an integration:** 27 | 28 | ```javascript 29 | 30 | client.pull('myTrello').then((stories) => { 31 | console.log(stories); 32 | }); 33 | 34 | client.pull('myGithub').then((stories) => { 35 | console.log(stories); 36 | }); 37 | 38 | client.pull('myJira').then((stories) => { 39 | console.log(stories); 40 | }); 41 | 42 | client.pull('myPivotal').then((stories) => { 43 | console.log(stories); 44 | }); 45 | 46 | client.pull('myTeuxdeux').then((stories) => { 47 | console.log(stories); 48 | }); 49 | 50 | client.pull('mySprintly').then((stories) => { 51 | console.log(stories); 52 | }); 53 | ``` 54 | 55 | **If you would like to push data to an integration:** 56 | 57 | ```javascript 58 | 59 | var stories = [ 60 | { 61 | name: 'I love creating Trello cards', 62 | description: 'Writing cards to any system using the same format is fun.' 63 | } 64 | ]; 65 | 66 | client.push('myTrello', stories).then((result) => { 67 | console.log(result); 68 | }); 69 | 70 | ``` 71 | 72 | # Super Common Story Format 73 | 74 | Super Common Story Format (SCSF) is an experimental attempt to unify task systems such that a common container format can be used as an interchange format between different systems. 75 | 76 | ## EXPERIMENTAL 77 | 78 | | **Field** | **Description** | **Example** | **JIRA** | **Trello** | **Pivotal** | **GitHub** | **Teuxdeux** | 79 | | ------ | ------ | ------ | ------ | ------ | ------ | ------ | ------ | 80 | | **meta.source.name** | A text identifier of the source service | trello | | "trello" | | | | 81 | | **meta.source.data** | Array of data from source | | | | | | | 82 | | **data.id** | unique identifier | 237623762 | id | id | id | id | id | 83 | | **data.self** | API url for item | http://example.com/api/item/5 | self | - | - | url | - | 84 | | **data.key** | alternate unique identifier | TSK-01 | key | idShort | | number | uuid | 85 | | **data.name** | summary of the item | do a thing | fields.summary | name | name | title | text | 86 | | **data.description** | full description of the item | We want do a thing not because it is easy but because it is hard. | fields.description | desc | | body | - | 87 | | **data.project.id** | parent identifier | 347632746 | fields.project.id | idBoard | project_id | this.integration.repo | | 88 | | **data.type** | type of item | task | issue | bug | feature | story | issuetype.name | - | kind | | | 89 | | **data.url** | url for item in it's native UI | http://example.com/item/5 | | url | urk | html_url | | 90 | | **data.archived** | is item archived indicator | yes | no | | | - | | | 91 | | **data.status** | working status of item | open | closed | backlog | current | | | current_state | state | done | 92 | | **data.date.start** | datetime and item should begin | | | | | | | 93 | | **data.date.end** | synonomous to date.due | | | | | | | 94 | | **data.date.due** | datetime item is due | 2015-08-14T15:05:55+00:00 | fields.duedate | due | | | | 95 | | **data.date.completed** | datetime item was completed | 2015-08-14T15:05:55+00:00 | | | | closed_at | | 96 | | **data.date.created** | datetime item was created | 2015-08-14T15:05:55+00:00 | fields.created | dateLastActivity | created_at | created_at | | 97 | | **data.date.updated** | datetime item was last updated | 2015-08-14T15:05:55+00:00 | fields.updated | dateLastActivity | updated_at | updated_at | deletedAt | 98 | | **data.date.deleted** | datetime and item was deleted | | | | | | start_date | 99 | | **data.lists** | array of lists/columns to which the story belongs | | | | | | | 100 | | **data.lists.list.id** | | | | | | | | 101 | | **data.lists.list.position** | | | | | | | | 102 | | **data.labels** | | | | | | | | 103 | | **data.email** | | | | | | | | 104 | | **data.shortUrl** | | | | | | | | 105 | -------------------------------------------------------------------------------- /dist/plugins/trello.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 6 | 7 | var SCSF = require('../scsf'); 8 | var trello = require('node-trello'); 9 | 10 | var Trello = (function () { 11 | function Trello(integration) { 12 | _classCallCheck(this, Trello); 13 | 14 | if (!('key' in integration)) { 15 | throw new Error('missing integration.key'); 16 | } 17 | if (!('token' in integration)) { 18 | throw new Error('missing integration.token'); 19 | } 20 | if (!('board_id' in integration)) { 21 | throw new Error('missing integration.board_id'); 22 | } 23 | this.integration = integration; 24 | this.headers = { 25 | 'User-Agent': 'Tuddy', 26 | 'Content-Type': 'application/json' 27 | }; 28 | } 29 | 30 | /** 31 | * Pulls card data from Trello 32 | */ 33 | 34 | _createClass(Trello, [{ 35 | key: 'pull', 36 | value: function pull() { 37 | var _this = this; 38 | 39 | return new Promise(function (resolve, reject) { 40 | var stories = []; 41 | var t = new trello(_this.integration.key, _this.integration.token); 42 | t.get('/1/boards/' + _this.integration.board_id + '/cards/all', function (err, data) { 43 | if (err) { 44 | return reject(err); 45 | } 46 | data.forEach(function (card) { 47 | stories.push(Trello.toSCSF(card)); 48 | }); 49 | return resolve(stories); 50 | }); 51 | }); 52 | } 53 | 54 | /** 55 | * Pushes cad data to toTrello 56 | * 57 | * @param stories 58 | */ 59 | }, { 60 | key: 'push', 61 | value: function push(stories) { 62 | var _this2 = this; 63 | 64 | return Promise.all(stories.map(function (story) { 65 | var t = new trello(_this2.integration.key, _this2.integration.token); 66 | var data = Trello.toTrello(story); 67 | data.idList = _this2.integration.idList; 68 | return new Promise(function (resolve, reject) { 69 | return t.post('/1/cards', data, function (err, body) { 70 | if (err) { 71 | return reject(err); 72 | } 73 | return resolve(Trello.toSCSF(body)); 74 | }); 75 | }); 76 | })); 77 | } 78 | 79 | /** 80 | * Convert SCSF data to Trello format 81 | */ 82 | }], [{ 83 | key: 'toTrello', 84 | value: function toTrello(input) { 85 | var merged = SCSF.merge({ data: input }); 86 | var data = merged.data; 87 | return { 88 | name: data.name, 89 | shortLink: data.key, 90 | url: data.url, 91 | desc: data.description, 92 | shortUrl: data.short_url, 93 | email: data.email, 94 | due: data.date.due, 95 | dateLastActivity: data.date.updated, 96 | closed: (function (data) { 97 | if (data.status == 'complete') { 98 | return true; 99 | } else { 100 | return false; 101 | } 102 | })(data) 103 | }; 104 | } 105 | 106 | /** 107 | * Converts Trello data to SCSF format 108 | */ 109 | }, { 110 | key: 'toSCSF', 111 | value: function toSCSF(data) { 112 | var trello = { 113 | meta: { 114 | source: { 115 | name: 'trello', 116 | data: data 117 | } 118 | }, 119 | data: { 120 | id: data.id, 121 | key: data.shortLink, 122 | url: data.url, 123 | name: data.name, 124 | description: data.desc, 125 | short_url: data.shortUrl, 126 | email: data.email, 127 | date: { 128 | due: data.due, 129 | updated: data.dateLastActivity 130 | }, 131 | status: (function (data) { 132 | switch (data.closed) { 133 | case false: 134 | return 'incomplete'; 135 | break; 136 | case true: 137 | return 'complete'; 138 | break; 139 | } 140 | })(data) 141 | } 142 | }; 143 | return SCSF.merge(trello); 144 | } 145 | }]); 146 | 147 | return Trello; 148 | })(); 149 | 150 | module.exports = Trello; -------------------------------------------------------------------------------- /lib/plugins/pivotal.js: -------------------------------------------------------------------------------- 1 | var SCSF = require('../scsf'); 2 | var url = require('url'); 3 | var request = require('request'); 4 | var Promise = require('bluebird'); 5 | var _ = require('lodash'); 6 | 7 | request.get = Promise.promisify(request.get, request); 8 | 9 | /** 10 | * Pivotal Tracker Interface 11 | */ 12 | class Pivotal { 13 | /** 14 | * Validates integration parameters and sets attributes 15 | * @param {object} integration Required parameters for integration 16 | */ 17 | constructor(integration) { 18 | if (!('project' in integration)) { 19 | throw new Error('missing integration.project'); 20 | } 21 | if (!('token' in integration)) { 22 | throw new Error('missing integration.token'); 23 | } 24 | this.integration = integration; 25 | this.headers = { 26 | 'X-TrackerToken': this.integration.token, 27 | 'Content-Type': 'application/json' 28 | }; 29 | } 30 | /** 31 | * Generates an API endpoint url 32 | * @param {object} data object containing parts of the url to replace 33 | * @return {string} url 34 | */ 35 | url(data) { 36 | let endpoint = `https://www.pivotaltracker.com/services/v5/projects/${this.integration.project}/stories`; 37 | 38 | let parsed = url.parse(endpoint, true); 39 | 40 | if (typeof data != 'undefined') { 41 | parsed.query = {}; 42 | delete parsed.search; 43 | if ('limit' in data) { 44 | parsed.query.limit = data.limit; 45 | } 46 | if ('offset' in data) { 47 | parsed.query.offset = data.offset; 48 | } 49 | } 50 | return parsed.format(parsed); 51 | } 52 | /** 53 | * Create stories at remote integration 54 | * @param {array} stories Stories to create 55 | * @return {array} Stories that were created 56 | */ 57 | push(stories) { 58 | return Promise.all(stories.map((story) => { 59 | return new Promise((resolve, reject) => { 60 | request.post(this.url(), { headers: this.headers, json: true, body: story }, (err, res, body) => { 61 | if (err) { 62 | return reject(err); 63 | } 64 | return resolve(Pivotal.toSCSF(body)); 65 | }); 66 | }); 67 | })); 68 | } 69 | /** 70 | * Retrieve stories from remote integration 71 | * @return {array} Stories retrieved from remote integration 72 | */ 73 | pull() { 74 | return this._findAll({}); 75 | } 76 | _query(query) { 77 | return request.get({ url: this.url(query), json: true, headers: this.headers }).spread(function(response, body){ 78 | var page = { 79 | total: parseInt(response.headers['x-tracker-pagination-total']), 80 | offset: parseInt(response.headers['x-tracker-pagination-offset']), 81 | returned: parseInt(response.headers['x-tracker-pagination-returned']), 82 | }; 83 | return [response, body, page]; 84 | }); 85 | } 86 | _findAll(query) { 87 | var stories = [], skip = 0, promises = []; 88 | var that = this; 89 | return this._query(query).spread(function (response, body, page) { 90 | if (body === undefined) { 91 | body = []; 92 | } 93 | body.forEach(function (story) { 94 | stories.push(story); 95 | }); 96 | skip = 0; 97 | while (skip < page.total) { 98 | skip += 100; 99 | var promise = that._query({ offset: skip }).spread(function (response, body, page) { 100 | body.forEach(function (story) { 101 | stories.push(story); 102 | }); 103 | return stories; 104 | }); 105 | promises.push(promise); 106 | } 107 | return Promise.settle(promises).spread(function () { 108 | return _.map(stories, function(story){ 109 | return Pivotal.toSCSF(story); 110 | }); 111 | }); 112 | }); 113 | } 114 | static toPivotal(input) { 115 | var merged = SCSF.merge({ data: input }); 116 | var data = merged.data; 117 | return { 118 | id: data.id, 119 | name: data.name, 120 | description: data.description, 121 | kind: 'story', 122 | story_type: 'feature', 123 | created_at: data.date.created, 124 | updated_at: data.date.updated, 125 | project_id: data.project.id, 126 | current_state: (function(data){ 127 | if (data == 'completed') { 128 | return 'finished'; 129 | } else { 130 | return 'unscheduled'; 131 | } 132 | })(data) 133 | } 134 | } 135 | static toSCSF(data) { 136 | var pivotal = { 137 | meta: { 138 | source: { 139 | name: 'pivotal', 140 | data: data 141 | } 142 | }, 143 | data: { 144 | id: data.id, 145 | name: data.name, 146 | description: data.description, 147 | url: data.url, 148 | type: data.story_type, 149 | status: (function(data){ 150 | switch (data.current_state) { 151 | case 'unscheduled': 152 | case 'started': 153 | case 'finished': 154 | case 'delivered': 155 | return 'incomplete'; 156 | break; 157 | case 'accepted': 158 | return 'complete'; 159 | break; 160 | } 161 | })(data), 162 | date: { 163 | created: data.created_at, 164 | updated: data.updated_at 165 | }, 166 | project: { 167 | id: data.project_id 168 | } 169 | } 170 | }; 171 | return SCSF.merge(pivotal); 172 | } 173 | } 174 | 175 | module.exports = Pivotal; 176 | -------------------------------------------------------------------------------- /test/trello.spec.js: -------------------------------------------------------------------------------- 1 | require('replay'); 2 | var expect = require('chai').expect; 3 | 4 | var Trello = require('../lib/plugins/trello.js'); 5 | 6 | describe('Trello', () => { 7 | describe('#toSCSF', () => { 8 | it('should correctly convert format', () => { 9 | var trello = { 10 | pos: 65535, 11 | descData: null, 12 | manualCoverAttachment: false, 13 | labels: [], 14 | idAttachmentCover: null, 15 | idLabels: [], 16 | idShort: 1, 17 | name: 'test1', 18 | desc: '', 19 | idMembersVoted: [], 20 | shortUrl: 'https://trello.com/c/ZNUzB6mn', 21 | idBoard: '55ccc49df22b549c64f3b91e', 22 | idChecklists: [], 23 | url: 'https://trello.com/c/ZNUzB6mn/1-test1', 24 | email: 'tuddy@example.com', 25 | checkItemStates: [], 26 | dateLastActivity: '2015-08-13T16:24:09.303Z', 27 | idMembers: [], 28 | idList: '55ccc4a5e08262688ef56a96', 29 | subscribed: false, 30 | shortLink: 'ZNUzB6mn', 31 | closed: false, 32 | badges: { 33 | votes: 0, 34 | viewingMemberVoted: false, 35 | subscribed: false, 36 | fogbugz: '', 37 | checkItems: 0, 38 | checkItemsChecked: 0, 39 | comments: 0, 40 | attachments: 0, 41 | description: false, 42 | due: null 43 | }, 44 | due: null, 45 | id: '55ccc4a9d3294f7a68cc90f3' 46 | }; 47 | 48 | var expected = { meta: { source: { id: null, name: 'trello', key: null } }, 49 | data: 50 | { id: '55ccc4a9d3294f7a68cc90f3', 51 | self: null, 52 | key: 'ZNUzB6mn', 53 | name: 'test1', 54 | description: '', 55 | type: null, 56 | url: 'https://trello.com/c/ZNUzB6mn/1-test1', 57 | archived: null, 58 | status: 'incomplete', 59 | email: 'tuddy@example.com', 60 | short_url: 'https://trello.com/c/ZNUzB6mn', 61 | date: 62 | { start: null, 63 | end: null, 64 | due: null, 65 | created: null, 66 | updated: '2015-08-13T16:24:09.303Z', 67 | completed: null, 68 | deleted: null }, 69 | lists: [], 70 | labels: [], 71 | project: { id: null }, 72 | document: { id: null, range: null }, 73 | users: [] } } 74 | 75 | var story = Trello.toSCSF(trello); 76 | expect(story.meta.source.data).to.be.an('object'); 77 | delete story.meta.source.data; 78 | expect(story).to.eql(expected) 79 | 80 | }); 81 | }); 82 | describe('#toTrello', () => { 83 | it('should correctly convert format', () => { 84 | var scsf = { 85 | id: '55ccc4a9d3294f7a68cc90f3', 86 | self: null, 87 | key: 'ZNUzB6mn', 88 | name: 'test1', 89 | description: '', 90 | type: null, 91 | url: 'https://trello.com/c/ZNUzB6mn/1-test1', 92 | archived: null, 93 | status: 'incomplete', 94 | email: 'tuddy@example.com', 95 | short_url: 'https://trello.com/c/ZNUzB6mn', 96 | date: { 97 | start: null, 98 | end: null, 99 | due: null, 100 | created: null, 101 | updated: '2015-08-13T16:24:09.303Z', 102 | completed: null, 103 | deleted: null 104 | }, 105 | lists: [], 106 | labels: [], 107 | project: { 108 | id: null 109 | }, 110 | users: [] 111 | }; 112 | var trello = Trello.toTrello(scsf); 113 | var expected = { 114 | name: 'test1', 115 | shortLink: 'ZNUzB6mn', 116 | url: 'https://trello.com/c/ZNUzB6mn/1-test1', 117 | desc: '', 118 | shortUrl: 'https://trello.com/c/ZNUzB6mn', 119 | email: 'tuddy@example.com', 120 | due: null, 121 | dateLastActivity: '2015-08-13T16:24:09.303Z', 122 | closed: false 123 | }; 124 | expect(trello).to.eql(expected); 125 | }); 126 | }); 127 | describe('#push', () => { 128 | it('should be able to push stories', (done) => { 129 | 130 | var trello = new Trello({ 131 | name: 'trello', 132 | type: 'trello', 133 | key: '87105c2d4268dd05af77c83b64326f8a', 134 | secret: '9c50679d726b6b6b449f1f3d9183deb09340353a714b2404c6f291b7e30db167', 135 | token: 'e6691ee3114053746f53504df00a736407b985ce3cd0b2957012fa702ce83065', 136 | board_id: '55ccc49df22b549c64f3b91e', 137 | idList: '55ccc4a5e08262688ef56a96' 138 | }); 139 | 140 | let stories = [ 141 | { 142 | name: 'test1' 143 | }, 144 | { 145 | name: 'test2' 146 | } 147 | ]; 148 | 149 | trello.push(stories).then((stories) => { 150 | expect(stories).to.have.length(2); 151 | done(); 152 | }).catch((err) => { 153 | done(err); 154 | }); 155 | 156 | }); 157 | }); 158 | describe('#pull', () => { 159 | it('should be able to pull stories', (done) => { 160 | 161 | var trello = new Trello({ 162 | name: 'trello', 163 | type: 'trello', 164 | key: '87105c2d4268dd05af77c83b64326f8a', 165 | secret: '9c50679d726b6b6b449f1f3d9183deb09340353a714b2404c6f291b7e30db167', 166 | token: 'e6691ee3114053746f53504df00a736407b985ce3cd0b2957012fa702ce83065', 167 | board_id: '55ccc49df22b549c64f3b91e', 168 | idList: '55ccc4a5e08262688ef56a96' 169 | }); 170 | 171 | trello.pull('trello').then((stories) => { 172 | expect(stories).to.have.length(4); 173 | done(); 174 | }).catch((err) => { 175 | done(err); 176 | }); 177 | 178 | }); 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /lib/plugins/github.js: -------------------------------------------------------------------------------- 1 | var SCSF = require('../scsf'); 2 | var _ = require('lodash'); 3 | var async = require('async'); 4 | var Promise = require('bluebird'); 5 | var request = require('request'); 6 | request.get = Promise.promisify(request.get, request); 7 | 8 | var url = require('url'); 9 | 10 | class Github { 11 | constructor(integration) { 12 | if (!('user' in integration)) { 13 | throw new Error('missing integration.user'); 14 | } 15 | if (!('repo' in integration)) { 16 | throw new Error('missing integration.repo'); 17 | } 18 | if (!('access_token' in integration)) { 19 | throw new Error('missing integration.access_token'); 20 | } 21 | this.integration = integration; 22 | this.headers = { 23 | 'User-Agent': 'Tuddy', 24 | 'Content-Type': 'application/json' 25 | } 26 | } 27 | url(data) { 28 | let user = this.integration.user; 29 | let repo = this.integration.repo; 30 | let access_token = this.integration.access_token; 31 | let endpoint = `https://api.github.com/repos/${user}/${repo}/issues?access_token=${access_token}`; 32 | 33 | let parsed = url.parse(endpoint, true); 34 | 35 | if (typeof data != 'undefined') { 36 | parsed.query = {}; 37 | delete parsed.search; 38 | if ('per_page' in data) { 39 | parsed.query.per_page = data.per_page; 40 | } 41 | if ('page' in data) { 42 | parsed.query.page = data.page; 43 | } 44 | if ('state' in data) { 45 | parsed.query.state = data.state; 46 | } 47 | } 48 | return parsed.format(parsed); 49 | } 50 | pull() { 51 | return this._findAll({ state: 'all' }); 52 | } 53 | push(stories) { 54 | return new Promise((resolve, reject) => { 55 | var that = this; 56 | var out = []; 57 | async.each(stories, function(story, callback){ 58 | var github = Github.toGithub(story); 59 | request.post({ url: that.url(), json: true, headers: that.headers, body: github }, function(err, response, body){ 60 | if (err) { 61 | callback(new Error(err)); 62 | } 63 | out.push(body); 64 | callback(); 65 | }); 66 | }, function(err) { 67 | var stories = _.map(out, function(story){ 68 | return Github.toSCSF(story); 69 | }); 70 | if (err) { 71 | return reject(err); 72 | } 73 | return resolve(stories); 74 | }); 75 | }); 76 | } 77 | static toGithub(input) { 78 | var merged = SCSF.merge({ data: input }); 79 | var data = merged.data; 80 | return { 81 | id: data.id, 82 | title: data.name, 83 | body: data.description, 84 | story_type: "feature", 85 | url: data.self, 86 | html_url: data.url, 87 | created_at: data.date.created, 88 | updated_at: data.date.updated, 89 | closed_at: data.date.completed, 90 | number: data.key, 91 | state: (function(data){ 92 | if (data.status == 'completed') { 93 | return 'open'; 94 | } else { 95 | return 'closed'; 96 | } 97 | })(data) 98 | } 99 | } 100 | /** 101 | * Converts Github data to SCSF format 102 | * @param {object} data Github data 103 | * @return {object} SCSF object 104 | */ 105 | static toSCSF(data) { 106 | var github = { 107 | meta: { 108 | source: { 109 | name: 'github', 110 | data: data 111 | } 112 | }, 113 | data: { 114 | id: data.id, 115 | key: data.number, 116 | self: data.url, 117 | url: data.html_url, 118 | name: data.title, 119 | description: data.body, 120 | date: { 121 | created: data.created_at, 122 | updated: data.updated_at, 123 | completed: data.closed_at 124 | }, 125 | status: (function(data){ 126 | switch (data.state) { 127 | case 'open': 128 | return 'incomplete'; 129 | break; 130 | case 'closed': 131 | return 'complete'; 132 | break; 133 | } 134 | })(data) 135 | } 136 | } 137 | return SCSF.merge(github); 138 | } 139 | _query(query) { 140 | let endpoint = query.url || this.url(query); 141 | return request.get({ url: endpoint, json: true, headers: this.headers }).spread(function(response, body){ 142 | var next = null; 143 | if (response.headers['link']) { 144 | var link = response.headers['link']; 145 | link.replace(/<([^>]*)>;\s*rel="([\w]*)\"/g, function(m, uri, type) { 146 | if (type == 'next') { 147 | next = uri; 148 | } 149 | }); 150 | } 151 | var page = { 152 | next: next 153 | }; 154 | return [response, body, page]; 155 | }).catch(function(err){ 156 | return new Error(err); 157 | }); 158 | } 159 | _findAll(query) { 160 | var stories = []; 161 | var that = this; 162 | 163 | function getData(query, callback) { 164 | that._query(query).spread(function (response, body, page) { 165 | if (body === undefined) { 166 | console.log('Github.findAll', query, response, body); 167 | body = []; 168 | } 169 | body.forEach(function (story) { 170 | stories.push(Github.toSCSF(story)); 171 | }); 172 | if (typeof page != 'undefined' && page.next) { 173 | query.url = page.next; 174 | getData(query, callback); 175 | } else { 176 | callback(); 177 | } 178 | }); 179 | } 180 | 181 | var def = Promise.defer(); 182 | 183 | getData(query, function(){ 184 | def.resolve(stories); 185 | }); 186 | 187 | return def.promise; 188 | 189 | } 190 | } 191 | 192 | module.exports = Github; 193 | -------------------------------------------------------------------------------- /lib/plugins/sprintly.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var async = require('async'); 3 | var Promise = require('bluebird'); 4 | var request = require('request'); 5 | 6 | request.get = Promise.promisify(request.get, request); 7 | request.post = Promise.promisify(request.post, request); 8 | 9 | var url = require('url'); 10 | 11 | class Sprintly { 12 | constructor(integration) { 13 | if (!('email' in integration)) { 14 | throw new Error('missing integration.email'); 15 | } 16 | if (!('key' in integration)) { 17 | throw new Error('missing integration.key'); 18 | } 19 | if (!('product' in integration)) { 20 | throw new Error('missing integration.product'); 21 | } 22 | this.integration = integration; 23 | this.headers = { 24 | 'User-Agent': 'Tuddy', 25 | 'Authorization': 'Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG' 26 | } 27 | } 28 | url(data) { 29 | let endpoint = `https://sprint.ly/api/products/${this.integration.product}/items.json`; 30 | 31 | let parsed = url.parse(endpoint, true); 32 | 33 | if (typeof data != 'undefined') { 34 | parsed.query = {}; 35 | delete parsed.search; 36 | // if ('per_page' in data) { 37 | // parsed.query.per_page = data.per_page; 38 | // } 39 | // if ('page' in data) { 40 | // parsed.query.page = data.page; 41 | // } 42 | // if ('state' in data) { 43 | // parsed.query.state = data.state; 44 | // } 45 | } 46 | return parsed.format(parsed); 47 | } 48 | pull() { 49 | return request.get({ url: this.url(), headers: this.headers }).spread((res, body) => { 50 | if (res.statusCode == 200) { 51 | return JSON.parse(body).map(Sprintly.toSCSF); 52 | } else { 53 | throw new Error(res.statusMessage); 54 | } 55 | }); 56 | // return this._findAll({}); 57 | } 58 | push(stories) { 59 | return Promise.all(stories.map((story) => { 60 | var a = { url: this.url(), headers: this.headers, formData: Sprintly.toSprintly(story) }; 61 | return request.post({ url: this.url(), headers: this.headers, formData: Sprintly.toSprintly(story) }).spread((res, body) => { 62 | if (res.statusCode == 200) { 63 | return Sprintly.toSCSF(JSON.parse(body)); 64 | } else { 65 | throw new Error(res.statusMessage); 66 | } 67 | }) 68 | })); 69 | // return new Promise((resolve, reject) => { 70 | // var that = this; 71 | // var out = []; 72 | // async.each(stories, function(story, callback){ 73 | // var Sprintly = Sprintly.toSprintly(story); 74 | // request.post({ url: that.url(), json: true, headers: that.headers, body: Sprintly }, function(err, response, body){ 75 | // if (err) { 76 | // console.log(err); 77 | // callback(new Error(err)); 78 | // } 79 | // body.fk = body.id; 80 | // body.id = story.id; 81 | // out.push(body); 82 | // callback(); 83 | // }); 84 | // }, function(err) { 85 | // var stories = _.map(out, function(story){ 86 | // return Sprintly.toSCSF(story); 87 | // }); 88 | // if (err) { 89 | // return reject(err); 90 | // } 91 | // return resolve(stories); 92 | // }); 93 | // }); 94 | } 95 | static toSprintly(data) { 96 | return { 97 | title: data.name, 98 | // description: data.description, 99 | type: data.type 100 | // status: data.status 101 | } 102 | } 103 | /** 104 | * Converts Sprintly data to SCSF format 105 | * @param {object} data Sprintly data 106 | * @return {object} SCSF object 107 | */ 108 | static toSCSF(data) { 109 | return { 110 | meta: { 111 | source: { 112 | name: 'github' 113 | } 114 | }, 115 | data: { 116 | id: data.number, 117 | name: data.title, 118 | description: data.description, 119 | type: data.type, 120 | url: data.short_url, 121 | short_url: data.short_url, 122 | status: data.status, 123 | date: { 124 | created: data.created_at, 125 | updated: data.updated_at 126 | }, 127 | project: { 128 | id: data.product.id, 129 | name: data.product.name 130 | } 131 | } 132 | } 133 | } 134 | _query(query) { 135 | let endpoint = query.url || this.url(query); 136 | return request.get({ url: endpoint, json: true, headers: this.headers }).spread(function(response, body){ 137 | var next = null; 138 | if (response.headers['link']) { 139 | var link = response.headers['link']; 140 | link.replace(/<([^>]*)>;\s*rel="([\w]*)\"/g, function(m, uri, type) { 141 | if (type == 'next') { 142 | next = uri; 143 | } 144 | }); 145 | } 146 | var page = { 147 | next: next 148 | }; 149 | return [response, body, page]; 150 | }).catch(function(err){ 151 | return new Error(err); 152 | }); 153 | } 154 | _findAll(query) { 155 | var stories = []; 156 | var that = this; 157 | 158 | function getData(query, callback) { 159 | that._query(query).spread(function (response, body, page) { 160 | if (body === undefined) { 161 | console.log('Sprintly.findAll', query, response, body); 162 | body = []; 163 | } 164 | body.forEach(function (story) { 165 | story.fk = story.id; 166 | story.id = null; 167 | stories.push(Sprintly.toSCSF(story)); 168 | }); 169 | if (typeof page != 'undefined' && page.next) { 170 | query.url = page.next; 171 | getData(query, callback); 172 | } else { 173 | callback(); 174 | } 175 | }); 176 | } 177 | 178 | var def = Promise.defer(); 179 | 180 | getData(query, function(){ 181 | def.resolve(stories); 182 | }); 183 | 184 | return def.promise; 185 | 186 | } 187 | } 188 | 189 | module.exports = Sprintly; 190 | -------------------------------------------------------------------------------- /dist/plugins/teuxdeux.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 6 | 7 | var SCSF = require('../scsf'); 8 | var cheerio = require('cheerio'); 9 | var Promise = require('bluebird'); 10 | var request = require('request'); 11 | request.get = Promise.promisify(request.get, request); 12 | request.post = Promise.promisify(request.post, request); 13 | request.put = Promise.promisify(request.put, request); 14 | 15 | var request = request.defaults({ jar: true }); 16 | 17 | var Teuxdeux = (function () { 18 | function Teuxdeux(integration) { 19 | _classCallCheck(this, Teuxdeux); 20 | 21 | if (!('username' in integration)) { 22 | throw new Error('missing integration.user'); 23 | } 24 | if (!('password' in integration)) { 25 | throw new Error('missing integration.pass'); 26 | } 27 | this.loginOperation = null; 28 | this.integration = integration; 29 | this.headers = { 30 | 'User-Agent': 'Tuddy', 31 | 'Content-Type': 'application/json' 32 | }; 33 | } 34 | 35 | /** 36 | * Private method to ensure that login occurs before another action 37 | * @return {[type]} [description] 38 | */ 39 | 40 | _createClass(Teuxdeux, [{ 41 | key: '_login', 42 | value: function _login() { 43 | var self = this; 44 | if (!self.loginOperation) { 45 | return self.loginOperation = request.get({ url: 'https://teuxdeux.com/login' }).spread(function (res, body) { 46 | var $ = cheerio.load(body); 47 | self.integration.authenticity_token = $('input[name=authenticity_token]').val(); 48 | }).then(function () { 49 | return request.post({ url: 'https://teuxdeux.com/login', form: self.integration }).spread(function (res, body) { 50 | return true; 51 | }); 52 | }); 53 | } 54 | return self.loginOperation; 55 | } 56 | 57 | /** 58 | * Retrieves stories from remote integration 59 | * @return {array} array of stories retrieved from remote integration 60 | */ 61 | }, { 62 | key: 'pull', 63 | value: function pull() { 64 | var _this = this; 65 | 66 | return this._login().then(function () { 67 | return request.get({ 68 | url: 'https://teuxdeux.com/api/v1/todos/calendar', 69 | json: true, 70 | headers: { 'x-csrf-token': _this.integration.authenticity_token } 71 | }).spread(function (res, body) { 72 | var stories = []; 73 | body.forEach(function (story) { 74 | stories.push(Teuxdeux.toSCSF(story)); 75 | }); 76 | return stories; 77 | }); 78 | }); 79 | } 80 | 81 | /** 82 | * Creates stories at remote integration 83 | * @param {array} stories array of stories to create 84 | * @return {array} array of stories that were created 85 | */ 86 | }, { 87 | key: 'push', 88 | value: function push(stories) { 89 | var _this2 = this; 90 | 91 | var out = []; 92 | return this._login().then(function () { 93 | return Promise.all(stories.map(function (story) { 94 | var data = Teuxdeux.toTeuxdeux(story); 95 | return request.post({ 96 | url: 'https://teuxdeux.com/api/v1/todos', 97 | form: data, 98 | json: true, 99 | headers: { 'x-csrf-token': _this2.integration.authenticity_token } 100 | }).spread(function (res, body) { 101 | out.push(Teuxdeux.toSCSF(body)); 102 | }); 103 | })).then(function () { 104 | return out; 105 | }); 106 | }); 107 | } 108 | 109 | /** 110 | * Converts SCSF to data to source format 111 | * @param {object} data SCSF data 112 | * @return {object} source data 113 | */ 114 | }], [{ 115 | key: 'toTeuxdeux', 116 | value: function toTeuxdeux(input) { 117 | var merged = SCSF.merge({ data: input }); 118 | var data = merged.data; 119 | return { 120 | id: data.id, 121 | text: data.name, 122 | uuid: data.key, 123 | done: (function (data) { 124 | if (data.status == 'complete') { 125 | return true; 126 | } else { 127 | return false; 128 | } 129 | })(data), 130 | current_date: data.date.start || new Date(), 131 | created_at: data.date.created, 132 | updated_at: data.date.updated 133 | }; 134 | } 135 | 136 | /** 137 | * Converts source data to SCSF format 138 | * @param {object} data source data 139 | * @return {object} transformed SCSF data 140 | */ 141 | }, { 142 | key: 'toSCSF', 143 | value: function toSCSF(data) { 144 | var teuxdeux = { 145 | meta: { 146 | source: { 147 | name: 'teuxdeux', 148 | data: data 149 | } 150 | }, 151 | data: { 152 | id: data.id, 153 | key: data.uuid, 154 | name: data.text, 155 | status: (function (data) { 156 | if (data.done == true) { 157 | return 'complete'; 158 | } else { 159 | return 'incomplete'; 160 | } 161 | })(data), 162 | date: { 163 | start: data.current_date, 164 | created: data.created_at, 165 | updated: data.updated_at 166 | } 167 | } 168 | }; 169 | return SCSF.merge(teuxdeux); 170 | } 171 | }]); 172 | 173 | return Teuxdeux; 174 | })(); 175 | 176 | module.exports = Teuxdeux; -------------------------------------------------------------------------------- /fixtures/api.github.com-443/143948298609163410: -------------------------------------------------------------------------------- 1 | GET /repos/tudddy/tuddy/issues?state=all 2 | content-type: application/json 3 | host: api.github.com 4 | accept: application/json 5 | 6 | HTTP/1.1 200 undefined 7 | server: GitHub.com 8 | date: Thu, 13 Aug 2015 16:23:06 GMT 9 | content-type: application/json; charset=utf-8 10 | content-length: 5733 11 | status: 200 OK 12 | x-ratelimit-limit: 60 13 | x-ratelimit-remaining: 52 14 | x-ratelimit-reset: 1439485226 15 | cache-control: public, max-age=60, s-maxage=60 16 | etag: "378a0d714ede231ab8c9d8724401b01a" 17 | vary: Accept 18 | x-github-media-type: github.v3 19 | x-xss-protection: 1; mode=block 20 | x-frame-options: deny 21 | content-security-policy: default-src 'none' 22 | access-control-allow-credentials: true 23 | access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 24 | access-control-allow-origin: * 25 | x-github-request-id: 183D2EF0:1CB9:2753CA0:55CCC469 26 | strict-transport-security: max-age=31536000; includeSubdomains; preload 27 | x-content-type-options: nosniff 28 | x-served-by: a241e1a8264a6ace03db946c85b92db3 29 | 30 | [{"url":"https://api.github.com/repos/tudddy/tuddy/issues/4","labels_url":"https://api.github.com/repos/tudddy/tuddy/issues/4/labels{/name}","comments_url":"https://api.github.com/repos/tudddy/tuddy/issues/4/comments","events_url":"https://api.github.com/repos/tudddy/tuddy/issues/4/events","html_url":"https://github.com/tudddy/tuddy/issues/4","id":100812367,"number":4,"title":"test3","user":{"login":"tudddy","id":13784755,"avatar_url":"https://avatars.githubusercontent.com/u/13784755?v=3","gravatar_id":"","url":"https://api.github.com/users/tudddy","html_url":"https://github.com/tudddy","followers_url":"https://api.github.com/users/tudddy/followers","following_url":"https://api.github.com/users/tudddy/following{/other_user}","gists_url":"https://api.github.com/users/tudddy/gists{/gist_id}","starred_url":"https://api.github.com/users/tudddy/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tudddy/subscriptions","organizations_url":"https://api.github.com/users/tudddy/orgs","repos_url":"https://api.github.com/users/tudddy/repos","events_url":"https://api.github.com/users/tudddy/events{/privacy}","received_events_url":"https://api.github.com/users/tudddy/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":0,"created_at":"2015-08-13T16:23:05Z","updated_at":"2015-08-13T16:23:05Z","closed_at":null,"body":null},{"url":"https://api.github.com/repos/tudddy/tuddy/issues/3","labels_url":"https://api.github.com/repos/tudddy/tuddy/issues/3/labels{/name}","comments_url":"https://api.github.com/repos/tudddy/tuddy/issues/3/comments","events_url":"https://api.github.com/repos/tudddy/tuddy/issues/3/events","html_url":"https://github.com/tudddy/tuddy/issues/3","id":100812366,"number":3,"title":"test4","user":{"login":"tudddy","id":13784755,"avatar_url":"https://avatars.githubusercontent.com/u/13784755?v=3","gravatar_id":"","url":"https://api.github.com/users/tudddy","html_url":"https://github.com/tudddy","followers_url":"https://api.github.com/users/tudddy/followers","following_url":"https://api.github.com/users/tudddy/following{/other_user}","gists_url":"https://api.github.com/users/tudddy/gists{/gist_id}","starred_url":"https://api.github.com/users/tudddy/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tudddy/subscriptions","organizations_url":"https://api.github.com/users/tudddy/orgs","repos_url":"https://api.github.com/users/tudddy/repos","events_url":"https://api.github.com/users/tudddy/events{/privacy}","received_events_url":"https://api.github.com/users/tudddy/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":0,"created_at":"2015-08-13T16:23:05Z","updated_at":"2015-08-13T16:23:05Z","closed_at":null,"body":null},{"url":"https://api.github.com/repos/tudddy/tuddy/issues/2","labels_url":"https://api.github.com/repos/tudddy/tuddy/issues/2/labels{/name}","comments_url":"https://api.github.com/repos/tudddy/tuddy/issues/2/comments","events_url":"https://api.github.com/repos/tudddy/tuddy/issues/2/events","html_url":"https://github.com/tudddy/tuddy/issues/2","id":100812247,"number":2,"title":"test2","user":{"login":"tudddy","id":13784755,"avatar_url":"https://avatars.githubusercontent.com/u/13784755?v=3","gravatar_id":"","url":"https://api.github.com/users/tudddy","html_url":"https://github.com/tudddy","followers_url":"https://api.github.com/users/tudddy/followers","following_url":"https://api.github.com/users/tudddy/following{/other_user}","gists_url":"https://api.github.com/users/tudddy/gists{/gist_id}","starred_url":"https://api.github.com/users/tudddy/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tudddy/subscriptions","organizations_url":"https://api.github.com/users/tudddy/orgs","repos_url":"https://api.github.com/users/tudddy/repos","events_url":"https://api.github.com/users/tudddy/events{/privacy}","received_events_url":"https://api.github.com/users/tudddy/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":0,"created_at":"2015-08-13T16:22:28Z","updated_at":"2015-08-13T16:22:28Z","closed_at":null,"body":""},{"url":"https://api.github.com/repos/tudddy/tuddy/issues/1","labels_url":"https://api.github.com/repos/tudddy/tuddy/issues/1/labels{/name}","comments_url":"https://api.github.com/repos/tudddy/tuddy/issues/1/comments","events_url":"https://api.github.com/repos/tudddy/tuddy/issues/1/events","html_url":"https://github.com/tudddy/tuddy/issues/1","id":100812234,"number":1,"title":"test1","user":{"login":"tudddy","id":13784755,"avatar_url":"https://avatars.githubusercontent.com/u/13784755?v=3","gravatar_id":"","url":"https://api.github.com/users/tudddy","html_url":"https://github.com/tudddy","followers_url":"https://api.github.com/users/tudddy/followers","following_url":"https://api.github.com/users/tudddy/following{/other_user}","gists_url":"https://api.github.com/users/tudddy/gists{/gist_id}","starred_url":"https://api.github.com/users/tudddy/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tudddy/subscriptions","organizations_url":"https://api.github.com/users/tudddy/orgs","repos_url":"https://api.github.com/users/tudddy/repos","events_url":"https://api.github.com/users/tudddy/events{/privacy}","received_events_url":"https://api.github.com/users/tudddy/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":0,"created_at":"2015-08-13T16:22:23Z","updated_at":"2015-08-13T16:22:23Z","closed_at":null,"body":""}] -------------------------------------------------------------------------------- /test/github.spec.js: -------------------------------------------------------------------------------- 1 | require('replay'); 2 | var expect = require('chai').expect; 3 | 4 | var Github = require('../lib/plugins/github.js'); 5 | 6 | describe('Github', () => { 7 | describe('#constructor', () => { 8 | it('should take integration from constructor and set it', () => { 9 | let integration = { repo: '', user: '', access_token: '' }; 10 | var github = new Github(integration); 11 | expect(github.integration).to.eql(integration); 12 | }); 13 | it('should set headers', () => { 14 | let integration = { repo: '', user: '', access_token: '' }; 15 | var github = new Github(integration); 16 | let headers = { 17 | 'User-Agent': 'Tuddy', 18 | 'Content-Type': 'application/json' 19 | }; 20 | expect(github.headers).to.eql(headers); 21 | }); 22 | }); 23 | describe('#url', () => { 24 | it('should generate an API url with querystring', () => { 25 | let integration = { repo: 'tuddy', user: 'tudddy', access_token: 'c136002525ae2a82dec9109c304167226f8f7e7e' }; 26 | var github = new Github(integration); 27 | var url = github.url({ per_page: 'thing' }); 28 | expect(url).to.eql('https://api.github.com/repos/tudddy/tuddy/issues?per_page=thing'); 29 | }); 30 | }); 31 | describe('#toSCSF', () => { 32 | it('should correctly convert data', () => { 33 | var github = { 34 | title: 'test3', 35 | milestone: null, 36 | url: 'https://api.github.com/repos/tudddy/tuddy/issues/4', 37 | labels: [], 38 | html_url: 'https://github.com/tudddy/tuddy/issues/4', 39 | created_at: '2015-08-13T16:23:05Z', 40 | comments: 0, 41 | updated_at: '2015-08-13T16:23:05Z', 42 | state: 'open', 43 | locked: false, 44 | user: { 45 | gists_url: 'https://api.github.com/users/tudddy/gists{/gist_id}', 46 | url: 'https://api.github.com/users/tudddy', 47 | type: 'User', 48 | avatar_url: 'https://avatars.githubusercontent.com/u/13784755?v=3', 49 | html_url: 'https://github.com/tudddy', 50 | organizations_url: 'https://api.github.com/users/tudddy/orgs', 51 | subscriptions_url: 'https://api.github.com/users/tudddy/subscriptions', 52 | site_admin: false, 53 | following_url: 'https://api.github.com/users/tudddy/following{/other_user}', 54 | followers_url: 'https://api.github.com/users/tudddy/followers', 55 | starred_url: 'https://api.github.com/users/tudddy/starred{/owner}{/repo}', 56 | login: 'tudddy', 57 | events_url: 'https://api.github.com/users/tudddy/events{/privacy}', 58 | gravatar_id: '', 59 | id: 13784755, 60 | repos_url: 'https://api.github.com/users/tudddy/repos', 61 | received_events_url: 'https://api.github.com/users/tudddy/received_events' 62 | }, 63 | closed_at: null, 64 | labels_url: 'https://api.github.com/repos/tudddy/tuddy/issues/4/labels{/name}', 65 | comments_url: 'https://api.github.com/repos/tudddy/tuddy/issues/4/comments', 66 | assignee: null, 67 | events_url: 'https://api.github.com/repos/tudddy/tuddy/issues/4/events', 68 | number: 4, 69 | id: 100812367, 70 | body: null 71 | }; 72 | 73 | var expected = { meta: { source: { id: null, name: 'github', key: null } }, 74 | data: 75 | { id: 100812367, 76 | self: 'https://api.github.com/repos/tudddy/tuddy/issues/4', 77 | key: 4, 78 | name: 'test3', 79 | description: null, 80 | type: null, 81 | url: 'https://github.com/tudddy/tuddy/issues/4', 82 | archived: null, 83 | status: 'incomplete', 84 | email: null, 85 | short_url: null, 86 | date: 87 | { start: null, 88 | end: null, 89 | due: null, 90 | created: '2015-08-13T16:23:05Z', 91 | updated: '2015-08-13T16:23:05Z', 92 | completed: null, 93 | deleted: null }, 94 | lists: [], 95 | labels: [], 96 | project: { id: null }, 97 | document: { id: null, range: null }, 98 | users: [] } }; 99 | 100 | var story = Github.toSCSF(github); 101 | expect(story.meta.source.data).to.be.an('object'); 102 | delete story.meta.source.data; 103 | expect(story).to.eql(expected); 104 | }); 105 | }); 106 | describe('#toGithub', () => { 107 | it('should correctly convert data', () => { 108 | var scsf = { 109 | id: 100812367, 110 | self: 'https://api.github.com/repos/tudddy/tuddy/issues/4', 111 | key: 12345, 112 | name: 'test3', 113 | description: 'description', 114 | type: null, 115 | url: 'https://github.com/tudddy/tuddy/issues/4', 116 | archived: null, 117 | status: 'incomplete', 118 | email: null, 119 | short_url: null, 120 | date: { 121 | start: null, 122 | end: null, 123 | due: null, 124 | created: '2015-08-13T16:23:05Z', 125 | updated: '2015-08-13T16:23:05Z', 126 | completed: null, 127 | deleted: null 128 | }, 129 | lists: [], 130 | labels: [], 131 | project: { 132 | id: null 133 | }, 134 | users: [] 135 | }; 136 | var github = Github.toGithub(scsf); 137 | var expected = { 138 | id: 100812367, 139 | title: 'test3', 140 | body: 'description', 141 | story_type: 'feature', 142 | url: 'https://api.github.com/repos/tudddy/tuddy/issues/4', 143 | html_url: 'https://github.com/tudddy/tuddy/issues/4', 144 | created_at: '2015-08-13T16:23:05Z', 145 | updated_at: '2015-08-13T16:23:05Z', 146 | closed_at: null, 147 | number: 12345, 148 | state: 'closed' 149 | }; 150 | expect(github).to.eql(expected); 151 | }); 152 | }); 153 | describe('#push', () => { 154 | it('should be able to push stories', (done) => { 155 | let integration = { repo: 'tuddy', user: 'tudddy', access_token: 'c136002525ae2a82dec9109c304167226f8f7e7e' }; 156 | var github = new Github(integration); 157 | let stories = [ 158 | { 159 | name: 'test3' 160 | }, 161 | { 162 | name: 'test4' 163 | } 164 | ]; 165 | github.push(stories).then((stories) => { 166 | expect(stories).to.have.length(2); 167 | done(); 168 | }); 169 | }); 170 | }); 171 | describe('#pull', () => { 172 | it('should be able to pull stories', (done) => { 173 | let integration = { repo: 'tuddy', user: 'tudddy', access_token: 'c136002525ae2a82dec9109c304167226f8f7e7e' }; 174 | var github = new Github(integration); 175 | github.pull().then((stories) => { 176 | expect(stories).to.have.length(4); 177 | done(); 178 | }); 179 | }); 180 | }); 181 | }); 182 | -------------------------------------------------------------------------------- /dist/plugins/pivotal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 6 | 7 | var SCSF = require('../scsf'); 8 | var _url = require('url'); 9 | var request = require('request'); 10 | var Promise = require('bluebird'); 11 | var _ = require('lodash'); 12 | 13 | request.get = Promise.promisify(request.get, request); 14 | 15 | /** 16 | * Pivotal Tracker Interface 17 | */ 18 | 19 | var Pivotal = (function () { 20 | /** 21 | * Validates integration parameters and sets attributes 22 | * @param {object} integration Required parameters for integration 23 | */ 24 | 25 | function Pivotal(integration) { 26 | _classCallCheck(this, Pivotal); 27 | 28 | if (!('project' in integration)) { 29 | throw new Error('missing integration.project'); 30 | } 31 | if (!('token' in integration)) { 32 | throw new Error('missing integration.token'); 33 | } 34 | this.integration = integration; 35 | this.headers = { 36 | 'X-TrackerToken': this.integration.token, 37 | 'Content-Type': 'application/json' 38 | }; 39 | } 40 | 41 | /** 42 | * Generates an API endpoint url 43 | * @param {object} data object containing parts of the url to replace 44 | * @return {string} url 45 | */ 46 | 47 | _createClass(Pivotal, [{ 48 | key: 'url', 49 | value: function url(data) { 50 | var endpoint = 'https://www.pivotaltracker.com/services/v5/projects/' + this.integration.project + '/stories'; 51 | 52 | var parsed = _url.parse(endpoint, true); 53 | 54 | if (typeof data != 'undefined') { 55 | parsed.query = {}; 56 | delete parsed.search; 57 | if ('limit' in data) { 58 | parsed.query.limit = data.limit; 59 | } 60 | if ('offset' in data) { 61 | parsed.query.offset = data.offset; 62 | } 63 | } 64 | return parsed.format(parsed); 65 | } 66 | 67 | /** 68 | * Create stories at remote integration 69 | * @param {array} stories Stories to create 70 | * @return {array} Stories that were created 71 | */ 72 | }, { 73 | key: 'push', 74 | value: function push(stories) { 75 | var _this = this; 76 | 77 | return Promise.all(stories.map(function (story) { 78 | return new Promise(function (resolve, reject) { 79 | request.post(_this.url(), { headers: _this.headers, json: true, body: story }, function (err, res, body) { 80 | if (err) { 81 | return reject(err); 82 | } 83 | return resolve(Pivotal.toSCSF(body)); 84 | }); 85 | }); 86 | })); 87 | } 88 | 89 | /** 90 | * Retrieve stories from remote integration 91 | * @return {array} Stories retrieved from remote integration 92 | */ 93 | }, { 94 | key: 'pull', 95 | value: function pull() { 96 | return this._findAll({}); 97 | } 98 | }, { 99 | key: '_query', 100 | value: function _query(query) { 101 | return request.get({ url: this.url(query), json: true, headers: this.headers }).spread(function (response, body) { 102 | var page = { 103 | total: parseInt(response.headers['x-tracker-pagination-total']), 104 | offset: parseInt(response.headers['x-tracker-pagination-offset']), 105 | returned: parseInt(response.headers['x-tracker-pagination-returned']) 106 | }; 107 | return [response, body, page]; 108 | }); 109 | } 110 | }, { 111 | key: '_findAll', 112 | value: function _findAll(query) { 113 | var stories = [], 114 | skip = 0, 115 | promises = []; 116 | var that = this; 117 | return this._query(query).spread(function (response, body, page) { 118 | if (body === undefined) { 119 | body = []; 120 | } 121 | body.forEach(function (story) { 122 | stories.push(story); 123 | }); 124 | skip = 0; 125 | while (skip < page.total) { 126 | skip += 100; 127 | var promise = that._query({ offset: skip }).spread(function (response, body, page) { 128 | body.forEach(function (story) { 129 | stories.push(story); 130 | }); 131 | return stories; 132 | }); 133 | promises.push(promise); 134 | } 135 | return Promise.settle(promises).spread(function () { 136 | return _.map(stories, function (story) { 137 | return Pivotal.toSCSF(story); 138 | }); 139 | }); 140 | }); 141 | } 142 | }], [{ 143 | key: 'toPivotal', 144 | value: function toPivotal(input) { 145 | var merged = SCSF.merge({ data: input }); 146 | var data = merged.data; 147 | return { 148 | id: data.id, 149 | name: data.name, 150 | description: data.description, 151 | kind: 'story', 152 | story_type: 'feature', 153 | created_at: data.date.created, 154 | updated_at: data.date.updated, 155 | project_id: data.project.id, 156 | current_state: (function (data) { 157 | if (data == 'completed') { 158 | return 'finished'; 159 | } else { 160 | return 'unscheduled'; 161 | } 162 | })(data) 163 | }; 164 | } 165 | }, { 166 | key: 'toSCSF', 167 | value: function toSCSF(data) { 168 | var pivotal = { 169 | meta: { 170 | source: { 171 | name: 'pivotal', 172 | data: data 173 | } 174 | }, 175 | data: { 176 | id: data.id, 177 | name: data.name, 178 | description: data.description, 179 | url: data.url, 180 | type: data.story_type, 181 | status: (function (data) { 182 | switch (data.current_state) { 183 | case 'unscheduled': 184 | case 'started': 185 | case 'finished': 186 | case 'delivered': 187 | return 'incomplete'; 188 | break; 189 | case 'accepted': 190 | return 'complete'; 191 | break; 192 | } 193 | })(data), 194 | date: { 195 | created: data.created_at, 196 | updated: data.updated_at 197 | }, 198 | project: { 199 | id: data.project_id 200 | } 201 | } 202 | }; 203 | return SCSF.merge(pivotal); 204 | } 205 | }]); 206 | 207 | return Pivotal; 208 | })(); 209 | 210 | module.exports = Pivotal; -------------------------------------------------------------------------------- /dist/plugins/github.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 6 | 7 | var SCSF = require('../scsf'); 8 | var _ = require('lodash'); 9 | var async = require('async'); 10 | var Promise = require('bluebird'); 11 | var request = require('request'); 12 | request.get = Promise.promisify(request.get, request); 13 | 14 | var _url = require('url'); 15 | 16 | var Github = (function () { 17 | function Github(integration) { 18 | _classCallCheck(this, Github); 19 | 20 | if (!('user' in integration)) { 21 | throw new Error('missing integration.user'); 22 | } 23 | if (!('repo' in integration)) { 24 | throw new Error('missing integration.repo'); 25 | } 26 | if (!('access_token' in integration)) { 27 | throw new Error('missing integration.access_token'); 28 | } 29 | this.integration = integration; 30 | this.headers = { 31 | 'User-Agent': 'Tuddy', 32 | 'Content-Type': 'application/json' 33 | }; 34 | } 35 | 36 | _createClass(Github, [{ 37 | key: 'url', 38 | value: function url(data) { 39 | var user = this.integration.user; 40 | var repo = this.integration.repo; 41 | var access_token = this.integration.access_token; 42 | var endpoint = 'https://api.github.com/repos/' + user + '/' + repo + '/issues?access_token=' + access_token; 43 | 44 | var parsed = _url.parse(endpoint, true); 45 | 46 | if (typeof data != 'undefined') { 47 | parsed.query = {}; 48 | delete parsed.search; 49 | if ('per_page' in data) { 50 | parsed.query.per_page = data.per_page; 51 | } 52 | if ('page' in data) { 53 | parsed.query.page = data.page; 54 | } 55 | if ('state' in data) { 56 | parsed.query.state = data.state; 57 | } 58 | } 59 | return parsed.format(parsed); 60 | } 61 | }, { 62 | key: 'pull', 63 | value: function pull() { 64 | return this._findAll({ state: 'all' }); 65 | } 66 | }, { 67 | key: 'push', 68 | value: function push(stories) { 69 | var _this = this; 70 | 71 | return new Promise(function (resolve, reject) { 72 | var that = _this; 73 | var out = []; 74 | async.each(stories, function (story, callback) { 75 | var github = Github.toGithub(story); 76 | request.post({ url: that.url(), json: true, headers: that.headers, body: github }, function (err, response, body) { 77 | if (err) { 78 | callback(new Error(err)); 79 | } 80 | out.push(body); 81 | callback(); 82 | }); 83 | }, function (err) { 84 | var stories = _.map(out, function (story) { 85 | return Github.toSCSF(story); 86 | }); 87 | if (err) { 88 | return reject(err); 89 | } 90 | return resolve(stories); 91 | }); 92 | }); 93 | } 94 | }, { 95 | key: '_query', 96 | value: function _query(query) { 97 | var endpoint = query.url || this.url(query); 98 | return request.get({ url: endpoint, json: true, headers: this.headers }).spread(function (response, body) { 99 | var next = null; 100 | if (response.headers['link']) { 101 | var link = response.headers['link']; 102 | link.replace(/<([^>]*)>;\s*rel="([\w]*)\"/g, function (m, uri, type) { 103 | if (type == 'next') { 104 | next = uri; 105 | } 106 | }); 107 | } 108 | var page = { 109 | next: next 110 | }; 111 | return [response, body, page]; 112 | })['catch'](function (err) { 113 | return new Error(err); 114 | }); 115 | } 116 | }, { 117 | key: '_findAll', 118 | value: function _findAll(query) { 119 | var stories = []; 120 | var that = this; 121 | 122 | function getData(query, callback) { 123 | that._query(query).spread(function (response, body, page) { 124 | if (body === undefined) { 125 | console.log('Github.findAll', query, response, body); 126 | body = []; 127 | } 128 | body.forEach(function (story) { 129 | stories.push(Github.toSCSF(story)); 130 | }); 131 | if (typeof page != 'undefined' && page.next) { 132 | query.url = page.next; 133 | getData(query, callback); 134 | } else { 135 | callback(); 136 | } 137 | }); 138 | } 139 | 140 | var def = Promise.defer(); 141 | 142 | getData(query, function () { 143 | def.resolve(stories); 144 | }); 145 | 146 | return def.promise; 147 | } 148 | }], [{ 149 | key: 'toGithub', 150 | value: function toGithub(input) { 151 | var merged = SCSF.merge({ data: input }); 152 | var data = merged.data; 153 | return { 154 | id: data.id, 155 | title: data.name, 156 | body: data.description, 157 | story_type: "feature", 158 | url: data.self, 159 | html_url: data.url, 160 | created_at: data.date.created, 161 | updated_at: data.date.updated, 162 | closed_at: data.date.completed, 163 | number: data.key, 164 | state: (function (data) { 165 | if (data.status == 'completed') { 166 | return 'open'; 167 | } else { 168 | return 'closed'; 169 | } 170 | })(data) 171 | }; 172 | } 173 | 174 | /** 175 | * Converts Github data to SCSF format 176 | * @param {object} data Github data 177 | * @return {object} SCSF object 178 | */ 179 | }, { 180 | key: 'toSCSF', 181 | value: function toSCSF(data) { 182 | var github = { 183 | meta: { 184 | source: { 185 | name: 'github', 186 | data: data 187 | } 188 | }, 189 | data: { 190 | id: data.id, 191 | key: data.number, 192 | self: data.url, 193 | url: data.html_url, 194 | name: data.title, 195 | description: data.body, 196 | date: { 197 | created: data.created_at, 198 | updated: data.updated_at, 199 | completed: data.closed_at 200 | }, 201 | status: (function (data) { 202 | switch (data.state) { 203 | case 'open': 204 | return 'incomplete'; 205 | break; 206 | case 'closed': 207 | return 'complete'; 208 | break; 209 | } 210 | })(data) 211 | } 212 | }; 213 | return SCSF.merge(github); 214 | } 215 | }]); 216 | 217 | return Github; 218 | })(); 219 | 220 | module.exports = Github; -------------------------------------------------------------------------------- /dist/plugins/sprintly.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 6 | 7 | var _ = require('lodash'); 8 | var async = require('async'); 9 | var Promise = require('bluebird'); 10 | var request = require('request'); 11 | 12 | request.get = Promise.promisify(request.get, request); 13 | request.post = Promise.promisify(request.post, request); 14 | 15 | var _url = require('url'); 16 | 17 | var Sprintly = (function () { 18 | function Sprintly(integration) { 19 | _classCallCheck(this, Sprintly); 20 | 21 | if (!('email' in integration)) { 22 | throw new Error('missing integration.email'); 23 | } 24 | if (!('key' in integration)) { 25 | throw new Error('missing integration.key'); 26 | } 27 | if (!('product' in integration)) { 28 | throw new Error('missing integration.product'); 29 | } 30 | this.integration = integration; 31 | this.headers = { 32 | 'User-Agent': 'Tuddy', 33 | 'Authorization': 'Basic amVmZit0dWRkeUBsb2lzZWxsZXMuY29tOldIQ1J2eGRqRTZhcHRuQmZqWjgzWldhVlFFam43R2VG' 34 | }; 35 | } 36 | 37 | _createClass(Sprintly, [{ 38 | key: 'url', 39 | value: function url(data) { 40 | var endpoint = 'https://sprint.ly/api/products/' + this.integration.product + '/items.json'; 41 | 42 | var parsed = _url.parse(endpoint, true); 43 | 44 | if (typeof data != 'undefined') { 45 | parsed.query = {}; 46 | delete parsed.search; 47 | // if ('per_page' in data) { 48 | // parsed.query.per_page = data.per_page; 49 | // } 50 | // if ('page' in data) { 51 | // parsed.query.page = data.page; 52 | // } 53 | // if ('state' in data) { 54 | // parsed.query.state = data.state; 55 | // } 56 | } 57 | return parsed.format(parsed); 58 | } 59 | }, { 60 | key: 'pull', 61 | value: function pull() { 62 | return request.get({ url: this.url(), headers: this.headers }).spread(function (res, body) { 63 | if (res.statusCode == 200) { 64 | return JSON.parse(body).map(Sprintly.toSCSF); 65 | } else { 66 | throw new Error(res.statusMessage); 67 | } 68 | }); 69 | // return this._findAll({}); 70 | } 71 | }, { 72 | key: 'push', 73 | value: function push(stories) { 74 | var _this = this; 75 | 76 | return Promise.all(stories.map(function (story) { 77 | var a = { url: _this.url(), headers: _this.headers, formData: Sprintly.toSprintly(story) }; 78 | return request.post({ url: _this.url(), headers: _this.headers, formData: Sprintly.toSprintly(story) }).spread(function (res, body) { 79 | if (res.statusCode == 200) { 80 | return Sprintly.toSCSF(JSON.parse(body)); 81 | } else { 82 | throw new Error(res.statusMessage); 83 | } 84 | }); 85 | })); 86 | // return new Promise((resolve, reject) => { 87 | // var that = this; 88 | // var out = []; 89 | // async.each(stories, function(story, callback){ 90 | // var Sprintly = Sprintly.toSprintly(story); 91 | // request.post({ url: that.url(), json: true, headers: that.headers, body: Sprintly }, function(err, response, body){ 92 | // if (err) { 93 | // console.log(err); 94 | // callback(new Error(err)); 95 | // } 96 | // body.fk = body.id; 97 | // body.id = story.id; 98 | // out.push(body); 99 | // callback(); 100 | // }); 101 | // }, function(err) { 102 | // var stories = _.map(out, function(story){ 103 | // return Sprintly.toSCSF(story); 104 | // }); 105 | // if (err) { 106 | // return reject(err); 107 | // } 108 | // return resolve(stories); 109 | // }); 110 | // }); 111 | } 112 | }, { 113 | key: '_query', 114 | value: function _query(query) { 115 | var endpoint = query.url || this.url(query); 116 | return request.get({ url: endpoint, json: true, headers: this.headers }).spread(function (response, body) { 117 | var next = null; 118 | if (response.headers['link']) { 119 | var link = response.headers['link']; 120 | link.replace(/<([^>]*)>;\s*rel="([\w]*)\"/g, function (m, uri, type) { 121 | if (type == 'next') { 122 | next = uri; 123 | } 124 | }); 125 | } 126 | var page = { 127 | next: next 128 | }; 129 | return [response, body, page]; 130 | })['catch'](function (err) { 131 | return new Error(err); 132 | }); 133 | } 134 | }, { 135 | key: '_findAll', 136 | value: function _findAll(query) { 137 | var stories = []; 138 | var that = this; 139 | 140 | function getData(query, callback) { 141 | that._query(query).spread(function (response, body, page) { 142 | if (body === undefined) { 143 | console.log('Sprintly.findAll', query, response, body); 144 | body = []; 145 | } 146 | body.forEach(function (story) { 147 | story.fk = story.id; 148 | story.id = null; 149 | stories.push(Sprintly.toSCSF(story)); 150 | }); 151 | if (typeof page != 'undefined' && page.next) { 152 | query.url = page.next; 153 | getData(query, callback); 154 | } else { 155 | callback(); 156 | } 157 | }); 158 | } 159 | 160 | var def = Promise.defer(); 161 | 162 | getData(query, function () { 163 | def.resolve(stories); 164 | }); 165 | 166 | return def.promise; 167 | } 168 | }], [{ 169 | key: 'toSprintly', 170 | value: function toSprintly(data) { 171 | return { 172 | title: data.name, 173 | // description: data.description, 174 | type: data.type 175 | // status: data.status 176 | }; 177 | } 178 | 179 | /** 180 | * Converts Sprintly data to SCSF format 181 | * @param {object} data Sprintly data 182 | * @return {object} SCSF object 183 | */ 184 | }, { 185 | key: 'toSCSF', 186 | value: function toSCSF(data) { 187 | return { 188 | meta: { 189 | source: { 190 | name: 'github' 191 | } 192 | }, 193 | data: { 194 | id: data.number, 195 | name: data.title, 196 | description: data.description, 197 | type: data.type, 198 | url: data.short_url, 199 | short_url: data.short_url, 200 | status: data.status, 201 | date: { 202 | created: data.created_at, 203 | updated: data.updated_at 204 | }, 205 | project: { 206 | id: data.product.id, 207 | name: data.product.name 208 | } 209 | } 210 | }; 211 | } 212 | }]); 213 | 214 | return Sprintly; 215 | })(); 216 | 217 | module.exports = Sprintly; -------------------------------------------------------------------------------- /fixtures/teuxdeux.com-443/144344327282465992: -------------------------------------------------------------------------------- 1 | GET /login 2 | host: teuxdeux.com 3 | 4 | HTTP/1.1 200 OK 5 | content-type: text/html;charset=utf-8 6 | date: Mon, 28 Sep 2015 12:27:52 GMT 7 | server: nginx/1.2.1 8 | set-cookie: rack.session=BAh7CEkiD3Nlc3Npb25faWQGOgZFRiJFNTA4YWZiZDdlZTdjOTViOWQ2YThi%0AZDg0ZDI5ODI1MmFlYThmNzMwNjcwMjdmMThiMWRlYTgyYTg1NDI5ZmNlNEki%0ADl9fRkxBU0hfXwY7AEZ7AEkiCWNzcmYGOwBGIkVkMTUxZjJhMWMzMWQ1YjUz%0ANzRmNjQxMTJiZTA3YzNjZDYzN2I5ZjhiNjQ1MGZhNTRlZTNkZDQ5Mjk0OGVl%0AZDBj%0A--5812153e55331c8d0ee375c1a31ca7e8fb682d6d; path=/; expires=Wed, 28 Oct 2015 12:27:52 -0000; secure; HttpOnly 9 | status: 200 OK 10 | x-frame-options: sameorigin, SAMEORIGIN 11 | x-xss-protection: 1; mode=block 12 | content-length: 6685 13 | connection: Close 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 |
74 |
75 | Loading notifications... |
76 |