Compare commits

..
675 Commits
Author SHA1 Message Date
Fosco Marotto 213bfea62d Merge pull request #1379 from ParsePlatform/fosco.226
Changelog and update to 2.2.6
2016-04-05 10:22:31 -07:00
Fosco Marotto a17e54c397 Changelog and update to 2.2.6 2016-04-05 10:20:48 -07:00
Florent Vilmart 5773143973 Disables find on installation from clients
- fixes #1372
2016-04-05 13:09:40 -04:00
Florent Vilmart 406f237119 Adds missing options to the CLI 2016-04-05 08:53:35 -04:00
Florent Vilmart bc384e97e1 Let travis build all branches 2016-04-05 08:53:17 -04:00
Blayne Chard 18906f17b4 Auth._loadRoles should not query the same role twice. 2016-04-04 21:32:41 -04:00
Fosco Marotto 442c723350 Merge pull request #1364 from ParsePlatform/fosco.225
Changelog and update to 2.2.5
2016-04-04 18:01:27 -07:00
Fosco Marotto 9053b941ee Changelog and update to 2.2.5 2016-04-04 17:47:20 -07:00
Fosco Marotto 7e52c18893 Merge pull request #1363 from ParsePlatform/issue/1319
Improves config loading and tests
2016-04-04 17:30:59 -07:00
Florent Vilmart 8c92b80b8f Improves config loading and tests 2016-04-04 20:02:34 -04:00
Fosco Marotto 27dd2ebf5d Merge pull request #1361 from ParsePlatform/fosco.npm
Adds travis configuration to deploy NPM on new version tags
2016-04-04 13:50:50 -07:00
Fosco Marotto c1ef0440b0 Merge pull request #1362 from peterbytes/patch-1
The Heroku link redirects to the wiki index
2016-04-04 13:50:31 -07:00
peterbytes f4889a3393 The Heroku link redirects to the wiki index
Changed the link so that it goes to Heroku Dev Center guide instead
2016-04-04 13:40:47 -07:00
Fosco Marotto 19da14f3df Adds travis configuration to deploy NPM on new version tags 2016-04-04 12:44:22 -07:00
Florent Vilmart 91de7504fe 🎉 fixes #1288
* ⚡ regression test for #1288

* 🎉 fixes #1288
2016-04-04 14:27:11 -04:00
Florent Vilmart ec6a39718d Adds console transport when testing with VERBOSE=1 2016-04-04 11:07:25 -07:00
Florent Vilmart 0e3636d880 Make notEqual work on relations
* ⚡ regression tests for #1349

* 🎉 fixes #1349

* Adds support for multiple constraints on the same key
2016-04-04 11:05:03 -07:00
Drew 89df778ac8 Merge pull request #1357 from ParsePlatform/schema-defaults
🚧 Inject the default schemas properties when loading it
2016-04-04 08:56:41 -07:00
Tommy Goode acc23d0828 Fix for #1334: using relative cloud code files broken
* Adding tests for absolute and relative cloud code file loading.

* Fixes #1334 by resolving relative cloud code file paths to the process' working directory.
2016-04-04 08:43:06 -04:00
Florent Vilmart 3a28352c96 🎉 fixes #1355
- injects default schema upon schema load
2016-04-04 08:36:40 -04:00
Jeremy May f99b5588ab Added session length option for session tokens to server configuration 2016-04-02 11:36:47 -04:00
Fosco Marotto 51664c8f33 Merge pull request #1268 from ParsePlatform/flovilmart.noLogsWhileTesting
Removes all stdout/err logs while testing
2016-04-01 13:42:50 -07:00
Fosco Marotto 8c12f80d28 Merge pull request #1269 from ParsePlatform/flovilmart.reportPushError
Improves report for Push error in logs and _PushStatus
2016-04-01 13:42:01 -07:00
Drew c346e15a18 Merge pull request #1280 from jeremyjackson89/master
Single object queries to use include and keys
2016-04-01 09:06:54 -07:00
Jeremy 4fd67a51fb Put the allowed query keys for get in a constant. 2016-04-01 11:51:42 -04:00
Jeremy fffc83c0d9 Added tests for include and key query parameters on a specific object. 2016-04-01 11:35:40 -04:00
Jeremy 04e8e3e233 Merge remote-tracking branch 'upstream/master' 2016-04-01 11:33:11 -04:00
Florent Vilmart 02af61dc9a Merge pull request #1315 from drew-gross/bool-only
Accept only bool for $exists in LiveQuery
2016-04-01 10:57:00 -04:00
Drew 57156cf52b Merge pull request #1314 from ParsePlatform/flovilmart.issue1302
Fixes issue #1302
2016-03-31 19:00:55 -07:00
Drew Gross 01af755d18 Accept only bool for $exists in LiveQuery 2016-03-31 18:49:58 -07:00
Drew 3f8a1aae02 Merge pull request #1250 from andrecardoso/live-query-server-matching-does-not-exist-queries
Matching queries with doesNotExist constraint
2016-03-31 18:42:17 -07:00
Florent Vilmart edfa4092c0 😎 fixes #1302
- when including elements from an array of pointers, filters unaccessible/missing objects
2016-03-31 20:39:09 -04:00
Florent Vilmart 431d864ac3 ⚡ reproduces #1302 2016-03-31 20:38:45 -04:00
Drew 647987534f Merge pull request #1312 from ParsePlatform/flovilmart.includesArray
Fixes bug related to include in queries
2016-03-31 16:49:15 -07:00
Florent Vilmart ca7d8580e3 👍 fixes #1298 2016-03-31 19:32:24 -04:00
Florent Vilmart 9c528c6fe8 🎉 regression test for #1298 2016-03-31 19:11:42 -04:00
Florent Vilmart 2b3bf7c2b7 ⚡ troubleshoot #1293 2016-03-31 19:01:42 -04:00
Florent Vilmart 6540ad1483 Merge pull request #1305 from ParsePlatform/flovilmart.emailAdapterConfig
Adds more options when using CLI/config
2016-03-31 16:20:44 -04:00
Florent Vilmart b5625bc2e5 more configuration options in the CLI 2016-03-31 11:53:25 -04:00
Florent Vilmart 09279d1987 Allows loading cli adapters from module path 2016-03-31 11:49:15 -04:00
Florent Vilmart dbf64a6d83 Merge pull request #1297 from drew-gross/error-copy
Update error message
2016-03-31 09:16:55 -04:00
Drew Gross eeb3331167 Update error message 2016-03-31 00:21:21 -07:00
Drew 8a6903aa8d Merge pull request #1287 from ParsePlatform/flovilmart.dynamicConfigMount
Adds ability to override mount with publicServerURL for production uses
2016-03-30 20:29:37 -07:00
Drew 434ac91129 Merge branch 'master' into flovilmart.dynamicConfigMount 2016-03-30 20:29:30 -07:00
Florent Vilmart f0ebb7bae6 Merge pull request #1295 from drew-gross/test-1259
Fixes #1271
2016-03-30 23:14:17 -04:00
Drew 781e81d36b Merge pull request #1291 from ParsePlatform/flovilmart.issue1257
Properly let masterKey add fields
2016-03-30 19:54:38 -07:00
Drew Gross 73bca3b64c Improve comments 2016-03-30 19:48:33 -07:00
Drew Gross 6311c95785 Fixes #1271 2016-03-30 19:35:54 -07:00
Florent Vilmart efa7366250 Merge pull request #1286 from drew-gross/test-1259
Regression test for #1259
2016-03-30 20:31:05 -04:00
Florent Vilmart 6b6e684d55 Merge pull request #1290 from ParsePlatform/drew-gross-patch-2
Point to #1271 as how to write a good issue report
2016-03-30 20:27:43 -04:00
Florent Vilmart 5d99075663 Properly let masterKey add fields 2016-03-30 20:27:12 -04:00
Drew c5636e1b77 Point to #1271 as how to write a good issue report 2016-03-30 17:09:11 -07:00
Florent Vilmart ab1858616b Adds ability to override mount with publicServerURL for production uses 2016-03-30 20:09:10 -04:00
Drew Gross 632c8054da Regression test for #1259 2016-03-30 16:46:08 -07:00
Florent Vilmart 7afc08abe4 Merge pull request #1283 from drew-gross/test-871
Regression test for #871
2016-03-30 19:17:17 -04:00
Florent Vilmart 33c9730be2 Merge pull request #1281 from drew-gross/test-701
Add a test to repro #701
2016-03-30 19:10:20 -04:00
Drew Gross 97d3deb73b Regression test for #871 2016-03-30 16:04:37 -07:00
Drew Gross a0e7739526 Add a test to repro #701 2016-03-30 11:43:47 -07:00
Jeremy 7064f1d3ed Allow queries on specific objects to user the include and keys query parameters. 2016-03-30 13:50:36 -04:00
Florent Vilmart 4774e90937 Removes all stdout/err logs while testing 2016-03-29 22:52:00 -04:00
Florent Vilmart 6055f2a552 Improves report for Push error in logs and _PushStatus 2016-03-29 22:42:37 -04:00
Fosco Marotto b2819df7e5 Merge pull request #1264 from ParsePlatform/fosco.224
Changelog and update to 2.2.4
2016-03-29 14:36:26 -08:00
Fosco Marotto 5dc5ef239e Merge pull request #1265 from drew-gross/ignore-thrown-message
Fix test issue cause by env variables
2016-03-29 14:36:07 -08:00
Fosco Marotto f25bcc490a Updated changelog 2016-03-29 15:31:36 -07:00
Florent Vilmart aa5ebd49f0 Merge pull request #1199 from yuzeh/fix-authData-null-value
Clean authData of null values on _User update
2016-03-29 18:28:17 -04:00
Fosco Marotto 106ac95622 Fix adapter imports 2016-03-29 15:27:43 -07:00
Fosco Marotto 1f4393cd4d Changelog and update to 2.2.4 2016-03-29 15:10:55 -07:00
Fosco Marotto e361ccf5a5 Merge pull request #1263 from drew-gross/fix-adapters
Fix adapter imports
2016-03-29 14:08:46 -08:00
Drew Gross 342a3a92fe Fix adapter imports 2016-03-29 15:05:03 -07:00
Dan Huang 507fa54570 Merge branch 'master' into fix-authData-null-value 2016-03-29 14:43:19 -07:00
yuzeh 1b8e613dfb Clean authData of null values on _User update
Adds a step to the RestWrite#execute chain: it cleans the response
authData object of null values.

For example, this:

{"authData": {"anonymous": null}, "updatedAt", ...}

will be transformed to this:

{"updatedAt", ...}

And this:

{"authData": {"anonymous": null, "twitter": ...}, "updatedAt", ...}

will be transformed to this:

{"authData": {"twitter": ...}, "updatedAt", ...}

Fixing this issue will fix anonymous user upgrades from the Android SDK.
2016-03-29 14:42:37 -07:00
Florent Vilmart f259a3b17e Supports increment as well as Increment 2016-03-29 14:42:37 -07:00
Fosco Marotto 6be067cff5 Merge pull request #1261 from ParsePlatform/fosco.223
Changelog and update to 2.2.3
2016-03-29 13:02:24 -08:00
Fosco Marotto 73f8bbc2b3 Changelog and update to 2.2.3 2016-03-29 13:49:23 -07:00
Drew 337d3c297a Merge pull request #1248 from ParsePlatform/flovilmart.incrementBadge
Supports increment as well as Increment
2016-03-29 12:49:48 -07:00
Florent Vilmart cb4b036947 Supports increment as well as Increment 2016-03-29 15:39:58 -04:00
Drew ef08dcf76c Merge pull request #1234 from ParsePlatform/flovilmart.winston
Better logging with winston
2016-03-29 10:54:36 -07:00
Drew 9dd860948a Merge pull request #1253 from DroidsOnRoids/master
Fixed bug with invalid email verification link on email update.
2016-03-29 10:41:49 -07:00
Krzysztof Zielonka 6dc7aa87c6 Fixed bug with 'undefined' token in email verification link 2016-03-29 19:02:19 +02:00
André Luiz Cardoso 3dccd61222 Matching queries with doesNotExist constraint 2016-03-29 11:28:45 -03:00
Florent Vilmart 592334756b Merge pull request #1235 from drew-gross/config-out-of-experimental
Tested with the dashboard, it now works great.
2016-03-28 21:58:11 -04:00
Florent Vilmart 247a06f399 use console.error on EADDRINUSE 2016-03-28 21:57:22 -04:00
Florent Vilmart 74b847f1d1 Merge pull request #1204 from ParsePlatform/flovilmart.flatteningPushAdapterResults
Improve flattening of results from pushAdapter
2016-03-28 21:56:42 -04:00
Drew Gross 75f49b4d1a Tested with the dashboard, it now works great. 2016-03-28 18:43:06 -07:00
Florent Vilmart 5c1fe3a325 Better logging with winston 2016-03-28 20:16:58 -04:00
Florent Vilmart cd05e3bd19 Merge pull request #1210 from framp/master
Make GlobalConfig work like parse.com
2016-03-26 23:57:48 -04:00
Federico Rampazzo 236c7d15c2 Refactor code 2016-03-27 04:32:58 +01:00
Federico Rampazzo 6b9fd16273 Added merging in Config with scoped set 2016-03-27 03:46:36 +01:00
Federico Rampazzo 94b10de7b8 Fixed config upsert implementation to handle nested object and __op:Delete 2016-03-27 00:32:57 +00:00
Florent Vilmart aeda714b87 Improve flattening of results from pushAdapter 2016-03-26 11:02:26 -04:00
Federico Rampazzo 114f7d1b58 Make global config visible from parse-dashboard when it's enabled 2016-03-26 11:15:17 +00:00
Florent Vilmart d47a756f80 Merge pull request #1195 from ParsePlatform/flovilmart.moduleParseServerPush
Push adapters are provided by external packages
2016-03-25 17:37:18 -04:00
Drew 6bea950b32 Merge pull request #1164 from drew-gross/push-enable
Mark push as enabled in serverInfo endpoint
2016-03-25 13:54:22 -07:00
Drew 1796ae3b7c Close code block in readme 2016-03-25 11:44:01 -07:00
Florent Vilmart 8731337677 removes onesignal 2016-03-25 14:20:12 -04:00
Florent Vilmart f2f7c8bad3 ParsePushAdapter is a package 2016-03-25 13:36:52 -04:00
Florent Vilmart e3cb473903 Merge pull request #1185 from ParsePlatform/flovilmart.arrayPointersInQuery
fixes problem affecting finding array pointers
2016-03-25 08:24:23 -04:00
Florent Vilmart ebfbe5794c Merge pull request #1188 from drew-gross/fix-flaky-test
Fix flaky test
2016-03-25 08:24:09 -04:00
Drew Gross 7cdf85b367 Use getter 2016-03-24 20:59:50 -07:00
Drew Gross b8b86264f9 Fix flaky test 2016-03-24 19:48:41 -07:00
Drew Gross ec35c39724 Fix bad rebase 2016-03-24 19:39:54 -07:00
Drew Gross 19e6293638 Mark push as enabled in serverInfo endpoint 2016-03-24 19:32:51 -07:00
Drew 7738d33399 Merge pull request #1144 from drew-gross/document-email-adapter
Document email adapter
2016-03-24 19:31:45 -07:00
Florent Vilmart 4ff52de01c fixes problem affecting finding array pointers 2016-03-24 20:42:06 -04:00
Drew Gross 27a3198d81 Fix imports 2016-03-24 15:08:44 -07:00
Drew Gross b4ee31322e Fix tests 2016-03-24 14:42:45 -07:00
Drew Gross 3e45e04cf1 Use version 1.0.0 of mailgun adapter 2016-03-24 14:42:45 -07:00
Drew Gross 958d77207a Fix typo 2016-03-24 14:42:10 -07:00
Drew Gross 259de3f104 Fix readme to note move of mailgun adapter 2016-03-24 14:42:10 -07:00
Drew Gross 152c7e88b8 Move mailgun adapter to it's own repo 2016-03-24 14:42:10 -07:00
Drew Gross f28b719b83 Fix tests 2016-03-24 14:41:13 -07:00
Drew Gross 2f00a02936 Document email adapter 2016-03-24 14:41:13 -07:00
Florent Vilmart f4ad4c1af5 Merge pull request #1172 from ParsePlatform/flovilmart.externalAdapters
Moves Files adapters to external packages
2016-03-24 14:46:15 -04:00
Florent Vilmart 83f5207d44 remove inaccurate comment 2016-03-24 14:15:00 -04:00
Drew 82ebba4db8 Merge pull request #1133 from carmenlau/reset-password-fix
Reset password fix
2016-03-24 10:35:02 -07:00
Florent Vilmart 3d3ed53c5f Updates test that is failing cuz of instanceof
- Something weird is happening here, instanceof is not giving the right value
2016-03-24 00:57:49 -04:00
Florent Vilmart 1e45ac753a Moves Files adapters to external packages 2016-03-24 00:48:34 -04:00
Carmen 603bf97c0b Add test case for checking _perishable_token, it should be unset after password reset. #951 2016-03-24 10:22:14 +08:00
Carmen 120c6feb61 Unset _perishable_token after reset password, instead of setting it to null. #951 2016-03-24 10:22:14 +08:00
Carmen b3c5e836d5 Clear reset password token after reset password. _perishable_token is not a parse field, cannot clear it through rest. Update it separately. #951 2016-03-24 10:22:14 +08:00
Carmen 53e152e975 Instead of executing write directly, reuse rest.update to fix request object in user before save is empty when reset password. #951 2016-03-24 10:22:14 +08:00
Fosco Marotto ee8f85baef Merge pull request #1169 from ParsePlatform/fosco.222
Changelog and update to 2.2.2
2016-03-23 16:57:53 -08:00
Fosco Marotto 7a73d74869 Changelog and update to 2.2.2 2016-03-23 17:57:12 -07:00
Mengyan Wang 047c8e71ff Merge pull request #1168 from ParsePlatform/wangmengyan.fix_gcm_payload
Add pushId back to GCM payload
2016-03-23 17:47:07 -07:00
wangmengyan95 cbd1b878b9 Add pushId back to GCM payload 2016-03-23 17:39:27 -07:00
Drew 24146753b9 Merge pull request #1166 from ParsePlatform/flovilmart.MoveParseServer
moves ParseServer to it's own file
2016-03-23 16:45:31 -07:00
Florent Vilmart ea211e658f moves ParseServer to it's own file 2016-03-23 19:05:56 -04:00
Florent Vilmart 16b6ec4879 Merge pull request #1153 from ParsePlatform/flovilmart.fixBrokenParseServerClass
Mounts createLiveQueryServer, fix babel induced problem
2016-03-23 12:49:41 -04:00
Florent Vilmart ddb76a8650 Mounts createLiveQueryServer, fix babel induced problem 2016-03-23 09:19:09 -04:00
Florent Vilmart f5fdedfbc7 Merge pull request #1138 from ParsePlatform/flovilmart.bootstrap
Adds bootstrap.sh
2016-03-23 07:52:17 -04:00
Florent Vilmart 3ce634bd82 Merge pull request #1142 from ParsePlatform/flovilmart.fixes1136
do not override username
2016-03-23 07:52:04 -04:00
Florent Vilmart b30805b47b do not override username 2016-03-22 21:17:53 -04:00
Fosco Marotto 0e662190b3 Merge pull request #1139 from ParsePlatform/drew-gross-patch-2
Update README.md - remove deploy buttons - replace with community links
2016-03-22 16:55:30 -08:00
Drew 7d6f359434 Update README.md 2016-03-22 17:17:37 -07:00
Florent Vilmart 0a4cc606c6 Adds curl test command 2016-03-22 20:10:13 -04:00
Fosco Marotto 8124abab34 Merge pull request #1137 from ParsePlatform/fosco.221
Changelog and update to 2.2.1
2016-03-22 15:56:03 -08:00
Florent Vilmart b7a723fa7e Adds bootstrap.sh 2016-03-22 18:59:41 -04:00
Fosco Marotto 4e39975239 Changelog and update to 2.2.1 2016-03-22 15:37:38 -07:00
Drew e506784741 Merge pull request #1132 from ParsePlatform/flovilmart.hotfixCLPAssignments
Generates default CLP, freezes objects
2016-03-22 14:39:48 -07:00
Florent Vilmart 5d19c4ab45 Generates default CLP, freezes objects 2016-03-22 08:12:58 -04:00
Florent Vilmart cc6c3f46fe Merge pull request #1128 from drew-gross/emable-schema-editor
Enabled CLP editing
2016-03-22 07:41:47 -04:00
Drew Gross ffbca83aa3 Enabled CLP editing 2016-03-21 16:20:12 -07:00
Florent Vilmart 4bd32c8a4b Merge pull request #980 from ParsePlatform/flovilmart.ParseServerClass
ParseServer is a class
2016-03-21 08:43:37 -04:00
Florent Vilmart 260cc21b67 ParseServer is a class 2016-03-21 08:32:22 -04:00
Florent Vilmart fb57d91b83 Merge pull request #1110 from ParsePlatform/flovilmart.missingSessionInstallationId
Properly sets installationId on creating session with 3rd party auth
2016-03-21 08:21:47 -04:00
Florent Vilmart 414af99644 Merge pull request #1114 from ParsePlatform/flovilmart.addUniqueObject
Adds support for plain object in $add, $addUnique, $remove
2016-03-21 08:21:30 -04:00
Florent Vilmart 134b6de2b1 Merge pull request #1098 from dtsolis/dtsolis.fsadapter-proper
Add FileSystemAdapter file adapter
2016-03-21 08:20:27 -04:00
Florent Vilmart a31baa4ebd differentiate signup and login createdWith.action on _Session 2016-03-20 15:58:17 -04:00
Florent Vilmart 3603b82eac Properly sets installationId on creating session with 3rd party auth 2016-03-20 15:58:16 -04:00
Florent Vilmart 937dde592f Adds support for plain object in $add, $addUnique, $remove 2016-03-20 15:57:54 -04:00
Fosco Marotto 24f0fffb41 Merge pull request #1111 from ParsePlatform/flovilmart.reduceConnectionCount
Reduces the number of connections to mongo created
2016-03-20 11:56:25 -08:00
Tsolis Dimitris Sotiris e4e7e42c65 Add FileSystemAdapter file adapter 2016-03-20 21:35:42 +02:00
Florent Vilmart 44add9d6ac Reduces the number of connections to mongo created 2016-03-20 10:34:55 -04:00
Florent Vilmart c39ba3bdc7 Merge pull request #1102 from ParsePlatform/flovilmart.vendorCoverage
Ignores uncovered mongodbURL
2016-03-19 15:47:25 -04:00
Florent Vilmart 13a33a9497 forces mongodb-runner 3.1.15 as 3.1.16 is problematic 2016-03-19 14:58:24 -04:00
Florent Vilmart 46c66667c1 Ignores uncovered mongodbURL 2016-03-19 13:27:37 -04:00
Fosco Marotto 8b7779a0a9 Merge pull request #1093 from ParsePlatform/fosco.220
Updating to 2.2.0 with Live Query launch
2016-03-18 15:11:52 -08:00
Fosco Marotto 9ce2c85622 Updating to 2.2.0 with Live Query launch 2016-03-18 12:54:42 -07:00
Fosco Marotto d5931f70b5 Merge pull request #1092 from ParsePlatform/wangmengyan.add_livequery_support
Add LiveQuery to parse server
2016-03-18 11:51:39 -08:00
wangmengyan95 555e25bf33 Add LiveQuery 2016-03-18 12:32:31 -07:00
Fosco Marotto cf3606246f Merge pull request #1086 from ParsePlatform/readme2
Expand env. variable list, misc copy edits
2016-03-18 09:45:23 -08:00
Fosco Marotto 5decaecd4f Merge pull request #1072 from ParsePlatform/fosco.user-cache
Clear the session-user cache when changing _User objects
2016-03-18 08:51:51 -08:00
Héctor Ramos 33984617c5 Expand env. variable list, misc copy edits 2016-03-17 17:19:48 -07:00
Florent Vilmart e2bcc87810 Merge pull request #1081 from ParsePlatform/flovilmart.deleteAuthData
Ensures _auth_data_[provider] gets deleted from document
2016-03-17 19:50:41 -04:00
Florent Vilmart 5ff186f212 adds test that ensure removed linked auth are actually deleted from document 2016-03-17 16:35:20 -04:00
Florent Vilmart 21fb14d8f4 Properly deletes authData.provider when set to null 2016-03-17 16:35:20 -04:00
Florent Vilmart 7194fb195c Merge pull request #1068 from ParsePlatform/flovilmart.AllowRolesOps
Allows Operations on Roles
2016-03-17 15:47:12 -04:00
Nikita Lutsenko ad2a29ed15 Merge pull request #1067 from ParsePlatform/nlutsenko.testing_routes
Unbreak and make specific testing routes work.
2016-03-17 10:47:46 -07:00
Benjamin Woodruff 14d3062602 Merge pull request #986 from ParsePlatform/mongo-uri-encode-auth
Add URI encoding to mongo auth parameters
2016-03-17 09:46:21 -07:00
Florent Vilmart 5c8ad833c7 Merge pull request #1001 from ParsePlatform/flovilmart.queryStringForEmailResets
Properly querystring encode the parameters
2016-03-17 08:16:46 -04:00
Florent Vilmart 9624970cd0 Merge pull request #1004 from ParsePlatform/flovilmart.PushStatus
Push Status API
2016-03-17 08:16:35 -04:00
Florent Vilmart 3837d33ee4 Merge pull request #1066 from ParsePlatform/flovilmart.returnsFullModificationsForREST
Improvements in REST calls
2016-03-17 08:16:18 -04:00
Florent Vilmart 7b3448ee52 Merge pull request #990 from ParsePlatform/flovilmart.nitCLI
serverURL is actually not required in CLI
2016-03-17 08:14:02 -04:00
Fosco Marotto 877d29daf8 Clear the session-user cache when changing _User objects 2016-03-17 02:06:26 -07:00
Florent Vilmart e270964b4b adds test for issue #1031 2016-03-17 00:04:52 -04:00
Florent Vilmart e1c475512c Returns updated keys when running with beforeSave 2016-03-16 23:48:52 -04:00
Florent Vilmart dd7fad8e96 Allows Operations on Roles 2016-03-16 23:07:51 -04:00
Florent Vilmart 40812e2f53 Merge pull request #979 from ParsePlatform/flovilmart.SupportDashboardLikeConfig
Adds support for apps key in config file, throws if length is > 1
2016-03-16 22:43:58 -04:00
Florent Vilmart 0f7335b534 Merge pull request #892 from ParsePlatform/flovilmart.httpRequestDefaultContentType
Default body to querystring, null encoding
2016-03-16 22:43:41 -04:00
Nikita Lutsenko 9ce019d128 Unbreak and make specific testing routes work. 2016-03-16 19:35:44 -07:00
Florent Vilmart ba545fbf59 Returns full modifications on PUT 2016-03-16 22:30:52 -04:00
Florent Vilmart 7346ff2b38 Merge pull request #1065 from ParsePlatform/flovilmart.hotfix-oauth-login
Makes sure the location in results has the proper objectId
2016-03-16 20:46:31 -04:00
Florent Vilmart 461ddcbd05 Makes sure the location in results has the proper objectId 2016-03-16 20:37:59 -04:00
Benjamin Woodruff 530fad5a58 Move mongoUrl to src/vendor
And add a README to src/vendor
2016-03-16 14:54:52 -07:00
Benjamin Woodruff 836582fdb1 Fix failing tests for uri encoding auth
This uses a *slightly* patched version of node's uri module to allow
commas and colons in hostnames, which causes the parsed representation
of replica sets to be less-awful.

Hostname are still somewhat broken in this representation, because we
have an array of hosts expressed as a list, but this is the
minimum-effort solution to getting format to be able to reprint a parsed
replica set correctly.

I understand that we probably don't want to merge this (for a lot of
reasons), but at least this shows exactly where the issue is, and yields
some useful discussion.
2016-03-16 14:31:44 -07:00
Benjamin Woodruff 81f3a79f04 Add (failing) tests for uri encoding auth 2016-03-16 14:31:44 -07:00
Benjamin Woodruff a30c81745a Add URI encoding to mongo auth parameters
The mongodb driver requires auth values be URI encoded:
https://github.com/mongodb/node-mongodb-native/commit/044063097dc4dd5e6cf3a3574c555fec7559d38b

This uses node's built-in url module to encode the auth portion, by
parsing and re-formatting it, which causes special characters to get URI
encoded properly:
https://nodejs.org/api/url.html#url_escaped_characters

This is all a bit silly since mongodb just takes our passed uri, and
runs it through the same url parser again, but not before explicitly
erroring on '@' characters in the uri.

This is similiar to #148 (reverted by #297), but with much less code,
and hopefully less breakage. Also, note that `uri_decode_auth` is no
longer needed. That was removed in the above referenced
node-mongodb-native commit.

I've tested this on usernames and passwords with @, !, +, and a space.

Presumably this would also work with usernames and passwords that are
already URI encoded (since parseUrl will simply unescape it, and
formatUrl will escape it again).
2016-03-16 14:31:44 -07:00
Héctor Ramos d6af77b170 Merge pull request #1058 from ParsePlatform/flovilmart-patch-1
Adds link to stack overflow community.
2016-03-16 09:34:43 -07:00
Florent Vilmart d45fbd7516 Adds link to stack overflow community.
Adds stack overflow community link
2016-03-16 10:44:57 -04:00
Florent Vilmart 86f93e39a7 supports keys marked as false 2016-03-15 20:22:48 -04:00
Florent Vilmart 6e4357c6df Adds support for apps key in config file, throws if length is > 1 2016-03-15 20:22:48 -04:00
Florent Vilmart 9ea344abb7 Merge pull request #1046 from ParsePlatform/flovilmart.fix1014
Fixes bug related to subqueries on unfetched objects
2016-03-15 19:26:13 -04:00
Florent Vilmart 3451fc3430 fixes same bug affecting $select and $dontSelect 2016-03-15 19:13:40 -04:00
Florent Vilmart 3ed3982e0e Fixes bug affecting matchesQuery and doesNotMatchQuery on relations on unfetched objects 2016-03-15 16:20:32 -04:00
Florent Vilmart e00112ec3c adds failing test for resolution 2016-03-15 15:19:41 -04:00
Héctor Ramos 3e893738ce Add migration phases screenshot for use in the wiki. 2016-03-14 17:41:37 -07:00
Héctor Ramos 4df9a7e786 Instructiosn moved to the wiki 2016-03-14 16:06:00 -07:00
Florent Vilmart ad9495bf14 Merge pull request #992 from ParsePlatform/flovilmart.afterSaveFullObjects
Sanitizes RestWrite.data before passing to inflated object
2016-03-14 10:57:33 -04:00
Florent Vilmart cc4e751682 Merge pull request #1021 from neverov/patch-1
Fixed database url parameter name in the doc
2016-03-14 10:57:17 -04:00
Florent Vilmart 18781f1f82 Removes _PushStatus from system classes, uses direct DB access to write 2016-03-14 10:20:24 -04:00
Florent Vilmart 0bd21cb808 Tweak test with special char 2016-03-14 08:43:27 -04:00
Andrey Neverov ba77d755e9 Fixed database url parameter name in the doc 2016-03-14 15:21:35 +03:00
Florent Vilmart 120f23c791 reverts to use binary APNs 2016-03-14 08:15:38 -04:00
Florent Vilmart 7c387e1ee9 Adds support to store push results 2016-03-13 23:37:13 -04:00
Florent Vilmart 05baf36a46 Fix tests, uses fork of http2 to support node 4.3.0 without ALPN
- the push server don't send TLS NPN and makes the http2 lib break
- the fork forces the next protocol negociation to h2 as we're sure
2016-03-13 23:35:32 -04:00
Florent Vilmart dad50d12f5 Upgrade APNS to use HTTP/2
- uses universal certificate
- removes tests logs
- standardized returned promises from APNS and GCM to something usable in _PushStatus
2016-03-13 18:15:15 -04:00
Florent Vilmart a392c088d8 Uses the resolved promise from the adapter 2016-03-12 15:30:12 -05:00
Florent Vilmart 4d401d9daa Stores the _PushStatus when sending push, set pending, and running states 2016-03-12 14:32:39 -05:00
Florent Vilmart 308fe1498a Centralizes list of system classes into Schema 2016-03-12 13:40:59 -05:00
Florent Vilmart 2557ea6300 Properly querystring encode the parameters 2016-03-12 10:34:57 -05:00
Florent Vilmart cadd6fe406 Sanitizes RestWrite.data before passing to inflated object 2016-03-11 23:03:47 -05:00
Fosco Marotto 49531e7efe Merge pull request #991 from ParsePlatform/readme-hotfix
Fix README formatting
2016-03-11 17:11:27 -08:00
Héctor Ramos 8a779210af Fix formatting 2016-03-11 17:06:37 -08:00
Florent Vilmart 5c880ca546 serverURL is actually not required 2016-03-11 20:03:40 -05:00
Héctor Ramos 2e41b30179 Merge pull request #988 from ParsePlatform/new-quickstart
Getting Started and Configuring Parse Server
2016-03-11 17:02:47 -08:00
Héctor Ramos 6c437ffb49 PR Feedback 2016-03-11 16:59:17 -08:00
Fosco Marotto a642fdeda9 Merge pull request #989 from ParsePlatform/fosco.216
Changelog and update for 2.1.6.
2016-03-11 16:58:35 -08:00
Héctor Ramos 91bcb2430c PR Feedback 2016-03-11 16:58:01 -08:00
Fosco Marotto ca0df3a27d Updating to 2.1.6 2016-03-11 16:49:06 -08:00
Héctor Ramos 2130d38d04 Expand Getting Started section, Configuration section. 2016-03-11 16:43:02 -08:00
Florent Vilmart 5ef12b55b4 fixes env:PORT 2016-03-11 19:29:51 -05:00
Drew 2e618f4258 Merge pull request #983 from ParsePlatform/flovilmart.PushQueryOperator
Full query support for badge Increment (#931)
2016-03-11 15:06:18 -08:00
Héctor Ramos c9a398f406 Merge branch 'master' of github.com:ParsePlatform/parse-server into new-quickstart 2016-03-11 13:52:44 -08:00
Florent Vilmart b89619b6ee Full query support for badge Increment 2016-03-11 16:23:58 -05:00
Florent Vilmart 7d787328c5 Merge pull request #952 from ParsePlatform/flovilmart.OAuthImprovements
AuthData logic refactor
2016-03-11 15:38:19 -05:00
Florent Vilmart daad05a00f removes key transformation for authData from restWrite, ensures authData is set in hooks 2016-03-11 15:14:53 -05:00
Florent Vilmart 2c991a4726 Merge pull request #956 from supersolid/steven.dboptions
Add database options to ParseServer constructor and pass to MongoStorageAdapter
2016-03-11 12:39:30 -05:00
steven-supersolid 6e65a8fc6f Add test for options being passed to MongoAdapter from DatabaseAdapter 2016-03-11 17:08:39 +00:00
Florent Vilmart cd7a4b80bf Merge pull request #978 from ParsePlatform/flovilmart.cleanupNullAuthDatakeysOnLogin
Fixes #935, cleans up authData null keys on login for android crash
2016-03-11 11:22:11 -05:00
Florent Vilmart 1ed868b99c Fixes #935, cleans up authData null keys on login for android crash 2016-03-11 11:14:58 -05:00
Florent Vilmart bcffcbade2 Fix when multiple authData keys are passed 2016-03-11 09:02:21 -05:00
Mengyan Wang d42d359437 Merge pull request #959 from ParsePlatform/wangmengyan.do_master_query_for_before/afterHook
Do master query for before/afterSaveHook
2016-03-10 22:47:04 -08:00
wangmengyan95 8ea2b615a4 Do master query for before/afterSaveHook 2016-03-10 22:38:07 -08:00
Florent Vilmart 1e7e4fe372 Merge pull request #898 from ParsePlatform/flovilmart.CLPAPI
Adds CLP API to Schema router
2016-03-11 00:32:41 -05:00
Florent Vilmart c935ed8364 Always return default public permissions 2016-03-10 23:01:45 -05:00
Florent Vilmart 16e3529c96 Removes blacklisting, *-but test case 2016-03-10 19:20:05 -05:00
Florent Vilmart 9c5f14981e Renames oauth to authDataManager in src 2016-03-10 18:14:56 -05:00
Florent Vilmart 54d154f7aa Centralizes AuthData validation 2016-03-10 18:14:56 -05:00
Florent Vilmart 9aaaf78a36 Anonymous is an OAuth 2016-03-10 18:14:56 -05:00
Florent Vilmart b1d399bf80 Adds blacklist permission, more test scenarios 2016-03-10 18:02:29 -05:00
Drew 2114153e08 Merge pull request #958 from raulr/graceful-shutdown
Shutdown standalone parse server gracefully
2016-03-10 12:35:55 -08:00
Raul Rodriguez 76e6f8c775 Shutdown standalone parse server gracefully 2016-03-10 20:05:28 +01:00
steven-supersolid dbf2afc5ea Add database options to ParseServer constructor and pass to MongoStorageAdapter 2016-03-10 16:49:45 +00:00
Florent Vilmart 2300b212d5 Merge pull request #946 from aneeshd16/patch-2
Changed FileLoggerAdapterSpec to fail gracefully on Windows
2016-03-10 11:34:55 -05:00
Florent Vilmart d71a58c217 Adds tests, improve coverage, adds ability to delete CLP with classLevelPermissions: null 2016-03-10 11:31:37 -05:00
Drew d2cba7502b Merge pull request #886 from aneeshd16/patch-1
Added test command for Windows support
2016-03-10 08:12:56 -08:00
Aneesh Devasthale dc0e8ebe9a Merge branch 'master' into patch-1 2016-03-10 16:24:22 +05:30
Aneesh Devasthale b47c927377 Changed FileLoggerAdapterSpec to fail gracefully on Windows
Running tests on Windows caused this error:
```
B:\Projects\Parse Server\parse-server\spec\FileLoggerAdapter.spec.js:38
        expect(results[0].message).toEqual('testing info logs');
                         ^

TypeError: Cannot read property 'message' of undefined
    at B:\Projects\Parse Server\parse-server\spec\FileLoggerAdapter.spec.js:38:26
    at ParsePromise.<anonymous> (B:\Projects\Parse Server\parse-server\src\Adapters\Logger\FileLoggerAdapter.js:9:17440)
    at ParsePromise.wrappedResolvedCallback (B:\Projects\Parse Server\parse-server\node_modules\parse\lib\node\ParsePromise.js:139:41)
    at ParsePromise.resolve (B:\Projects\Parse Server\parse-server\node_modules\parse\lib\node\ParsePromise.js:72:36)
    at resolveOne (B:\Projects\Parse Server\parse-server\node_modules\parse\lib\node\ParsePromise.js:471:29)
    at ParsePromise.object.then.errors.(anonymous function) (B:\Projects\Parse Server\parse-server\node_modules\parse\lib\node\ParsePromise.js:480:13)
    at ParsePromise.wrappedResolvedCallback (B:\Projects\Parse Server\parse-server\node_modules\parse\lib\node\ParsePromise.js:139:41)
    at ParsePromise.resolve (B:\Projects\Parse Server\parse-server\node_modules\parse\lib\node\ParsePromise.js:72:36)
    at ReadFileContext.callback (B:\Projects\Parse Server\parse-server\src\Adapters\Logger\FileLoggerAdapter.js:9:16189)
    at FSReqWrap.readFileAfterOpen [as oncomplete] (fs.js:303:13)
```
Rest of the tests could not be run as the test runner would break here. This change adds a check to fail when the FileLoggerAdapter returns an empty array from here: https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Logger/FileLoggerAdapter.js#L191

Regarding the cause of the error itself, it is due to different filename separators in *nix and Windows. The FileLoggerAdapter would not save logs (have not tested this). This is a separate issue and should also be fixed.
2016-03-10 11:17:52 +05:30
Florent Vilmart f08b0b32bb Merge pull request #944 from ParsePlatform/flovilmart.hotfixShebang
re-add shebang
2016-03-09 21:40:51 -05:00
Florent Vilmart ddd1ae3338 Validates key, values and operation in CLP 2016-03-09 20:19:12 -05:00
Florent Vilmart 78425c8a31 re-add shebang 2016-03-09 20:04:42 -05:00
Héctor Ramos c9e5a6a72a Merge branch 'master' of github.com:ParsePlatform/parse-server into new-quickstart 2016-03-09 16:37:07 -08:00
Héctor Ramos bec49e7dc2 Height 2016-03-09 16:36:41 -08:00
Florent Vilmart e75d233b7e Adds validation of addFields 2016-03-09 19:33:48 -05:00
Florent Vilmart 64f9fad285 Adds addField in CLP valid keys 2016-03-09 19:33:47 -05:00
Florent Vilmart 5780c1e425 Merges CLP endpoints with POST, PUT and GET 2016-03-09 19:33:47 -05:00
Florent Vilmart d4fd73100c Adds CLP API to Schema router 2016-03-09 19:33:47 -05:00
Nikita Lutsenko 51862c1265 Merge pull request #943 from ParsePlatform/nlutsenko.schemaCollection
Add new schema collection type and replace all usages of direct mongo collection for schema operations.
2016-03-09 16:27:36 -08:00
Nikita Lutsenko d86f0a8c69 Use schema collection instead of adaptive collection for all schema operations. 2016-03-09 15:21:29 -08:00
Nikita Lutsenko 2730398b92 Add new MongoSchemaCollection class that manages schemas for all collections. 2016-03-09 15:20:59 -08:00
Fosco Marotto 72362bc604 Merge pull request #933 from ParsePlatform/fosco.215
Changelog and update to 2.1.5
2016-03-09 13:01:27 -08:00
Fosco Marotto d594f93567 Updating to 2.1.5 2016-03-09 08:29:49 -08:00
Aneesh Devasthale bb5cd080bc Merge branch 'master' into patch-1 2016-03-09 18:41:14 +05:30
Drew 21444b07e4 Merge pull request #925 from ParsePlatform/nlutsenko.dontSelectQueries
Do not require where clause in  condition on queries.
2016-03-08 18:24:12 -08:00
Nikita Lutsenko e93873f7b1 Do not require where clause in condition on queries. 2016-03-08 17:08:27 -08:00
Drew 3723b9bb75 Merge pull request #924 from ParsePlatform/nlutsenko.hooks.acl
Make sure that ACLs propagate to before/after save hooks.
2016-03-08 16:29:53 -08:00
Nikita Lutsenko 0f07c5204e Add test validating that we have ACL propagate to before/after save hooks. 2016-03-08 16:15:49 -08:00
Nikita Lutsenko b9f08d9694 Do not mutate parameter object in DatabaseController. 2016-03-08 16:08:41 -08:00
Drew 7143a12a46 Merge pull request #919 from Marco129/minimize-query
Minimize db query
2016-03-08 10:52:17 -08:00
Marco129 2f63c5528d Minimize db query 2016-03-09 02:15:27 +08:00
Florent Vilmart bd7d951ad6 default encoding is now querystring instead of JSON 2016-03-08 08:12:30 -05:00
Florent Vilmart 6acb5cee80 ES6-ify 2016-03-08 08:04:34 -05:00
Florent Vilmart 982fc72327 Adds HTTPResponse object and lazy loading text and JSON 2016-03-08 07:56:17 -05:00
Florent Vilmart f40efe3923 Sets encoding to null to preserve raw buffers 2016-03-08 07:55:44 -05:00
Florent Vilmart 8300dd8a47 Encodes the body by default to application/json if is object 2016-03-08 07:54:23 -05:00
Florent Vilmart 0c446b9731 Merge pull request #912 from carmenlau/911-params-option
#911 support params option in Parse.Cloud.httpRequest
2016-03-08 07:52:54 -05:00
Carmen 440099267d #911 support params option in Parse.Cloud.httpRequest 2016-03-08 20:23:55 +08:00
Nikita Lutsenko 241cd8c735 Merge pull request #909 from ParsePlatform/nlutsenko.databaseController
Move DatabaseController and Schema fully to adaptive mongo collection.
2016-03-08 01:10:55 -08:00
Nikita Lutsenko 49eb9df1ef Remove private Schema API usage from SchemasRouter. 2016-03-08 00:53:51 -08:00
Nikita Lutsenko fb5b8fb58f Migrate Schema.js to adaptive mongo collection. 2016-03-08 00:53:51 -08:00
Nikita Lutsenko a163327ac9 Remove usages of non-adaptive collection inside DatabaseController. 2016-03-08 00:53:51 -08:00
Nikita Lutsenko 8d10447c9f Merge pull request #908 from ParsePlatform/nlutsenko.geoPoint
Fix flaky Parse.GeoPoint test.
2016-03-07 23:37:42 -08:00
Nikita Lutsenko 0abd5a5931 Re-enable GeoPoint test. 2016-03-07 22:47:18 -08:00
Nikita Lutsenko 438cf58d4c Fix early server response in Schema validation. 2016-03-07 22:47:18 -08:00
Nikita Lutsenko 98769a2226 Merge pull request #902 from ParsePlatform/flovilmart.FixPushNotifications
Increment badge the right way
2016-03-07 20:44:24 -08:00
Florent Vilmart 80869749aa Merge pull request #708 from mcdonamp/mcdonald-gcs-adapter
Yet Another FileAdapter: Google Cloud Storage
2016-03-07 23:26:21 -05:00
Florent Vilmart bf96f0d28a Fixes problems related to increment badge
- name conventions are aweful in PushController
- properly looks at the badge into body.data instead of body
- We may want to refactor that as it's confusing to use a full body
2016-03-07 22:07:15 -05:00
Florent Vilmart 4259c5d377 Merge pull request #903 from ParsePlatform/nlutsenko.push
Cleanup PushController/PushRouter, remove raw mongo collection access.
2016-03-07 20:42:21 -05:00
Nikita Lutsenko 654a540b6a Fix race condition in GlobalConfig test. 2016-03-07 17:22:27 -08:00
Nikita Lutsenko de0f71cc9e Remove useless masterKey validation in PushController.sendPush. 2016-03-07 17:22:27 -08:00
Nikita Lutsenko 47061d8e98 Migrate PushRouter to shared master-key middleware. 2016-03-07 17:22:27 -08:00
Nikita Lutsenko f2ead46580 Remove .rawCollection method from DatabaseController. 2016-03-07 17:22:27 -08:00
Nikita Lutsenko 7909f0efe0 Merge pull request #900 from drew-gross/schemas-500
Handle legacy _client_permissions key in _SCHEMA. Fixes #888.
2016-03-07 17:08:58 -08:00
Nikita Lutsenko 477e978e2f Merge pull request #901 from ParsePlatform/nlutsenko.config
Migrate ParseGlobalConfig to new database storage API.
2016-03-07 14:50:26 -08:00
Nikita Lutsenko fa6954169e Migrate ParseGlobalConfig.spec to new database storage API. 2016-03-07 14:11:43 -08:00
Nikita Lutsenko cea4b2bd6a Migrate and fix GlobalConfig database storage. 2016-03-07 14:06:46 -08:00
Drew Gross 963811d022 Handle legacy _client_permissions key in _SCHEMA. Fixes #888. 2016-03-07 13:44:45 -08:00
Nikita Lutsenko f1f9bde385 Merge pull request #844 from ParsePlatform/nlutsenko.hooks
Move HooksController to use MongoCollection instead of direct Mongo access.
2016-03-07 13:20:41 -08:00
Florent Vilmart 75ae95815d Merge pull request #887 from ParsePlatform/flovilmart.fixDoubleRelationPointerEqualContraint
Fixes bug when querying equalTo on objectId and relation
2016-03-07 10:37:49 -05:00
Fosco Marotto 2e8f34918e Merge pull request #878 from ParsePlatform/fosco.roles827
Adding a role scenario test for issue 827
2016-03-07 06:17:34 -08:00
Florent Vilmart 5cdcadea36 Fixes bug when querying equalTo on objectId and relation
- Adds $eq operator in transform
- Makes $eq operator on objectId when adding $in operator
2016-03-07 08:26:35 -05:00
Aneesh Devasthale 5b1cf9898d Added test command for Windows support
Use `npm run test:win` to run tests on Windows. Fixes #323.
Variables like `$COVERAGE_OPTION` are not supported in Windows.
2016-03-07 18:09:59 +05:30
Mike McDonald 2c5144028b Added tests to adapter loader, cleaned up README, renamed to GCS_BUCKET from GCS_BUCKET_NAME 2016-03-07 00:47:08 -08:00
Mike McDonald 0f00d659cb Removed extraneous console.log() 2016-03-07 00:34:41 -08:00
Mike McDonald ce35b81cc6 New things for GCS Adapter 2016-03-07 00:30:21 -08:00
Drew 53e7337eee Merge pull request #881 from Marco129/fix-delete-field
Improve delete flow for non-existence _Join collection
2016-03-06 22:42:56 -08:00
Marco129 91ff816839 Improve delete flow for non-existence _Join collection 2016-03-07 14:14:28 +08:00
Fosco Marotto 3a06117fa1 Adding a role scenario test for issue 827 2016-03-06 18:28:29 -08:00
Florent Vilmart b36fd6f245 Merge pull request #876 from ParsePlatform/flovilmart.fileUpload
Allow crossdomain on filesRouter
2016-03-06 19:03:46 -05:00
Mike McDonald 84635352e3 Merge branch 'master' of https://github.com/ParsePlatform/parse-server into mcdonald-gcs-adapter
* 'master' of https://github.com/ParsePlatform/parse-server:
  Remove limit when counting results.
  beforeSave changes should propagate to the response
  Fix delete relation field when _Join collection not exist
  Test empty authData block on login for #413
  Fix for related query on non-existing column
  Fix Markdown format: make checkboxes visible
  Fix create wrong _Session for Facebook login
  Modified the npm dev script to support Windows
  Improves tests, ensure unicity of roleIds
  Fix reversed roles lookup
  Fix leak warnings in tests, use mongodb-runner from node_modules
  Improves documentation, add loading tests
  Improves loading of Push Adapter, fix loading of S3Adapter
  Adds public_html and views for packaging
  Removes shebang for windows
  Better support for windows builds
  Fix add field to system schema
  Convert Schema.js to ES6 class.
2016-03-06 15:34:40 -08:00
Fosco Marotto e92ee7e1c2 Merge pull request #863 from ParsePlatform/fosco.authData2
Test empty authData block on login for #413
2016-03-06 14:35:23 -08:00
Fosco Marotto 157e43f4a4 Merge pull request #861 from ParsePlatform/fosco.role1
Fix for related query on non-existing column
2016-03-06 14:35:03 -08:00
Florent Vilmart f23aed522a Allow crossdomain on filesRouter 2016-03-06 17:29:09 -05:00
Florent Vilmart 375e7016be Merge pull request #833 from ParsePlatform/flovilmart.S3Environment
Improves loading of Push Adapter, fix loading of S3Adapter
2016-03-06 16:13:05 -05:00
Florent Vilmart 00d0d3ea7c Merge pull request #841 from ParsePlatform/flovilmart.Roles
Fix reversed roles lookup
2016-03-06 15:42:23 -05:00
Fosco Marotto 3fb6a60977 Merge pull request #867 from ParsePlatform/fosco.count
Remove limit when counting results.
2016-03-06 12:39:39 -08:00
Fosco Marotto fedeff9a20 Merge pull request #864 from Marco129/fix-delete-field
Fix delete relation field when _Join collection not exist
2016-03-06 04:12:08 -08:00
Fosco Marotto 1450795516 Remove limit when counting results. 2016-03-06 03:32:49 -08:00
Fosco Marotto 7e153d48f4 Merge pull request #865 from ParsePlatform/fosco.457
beforeSave changes should propagate to the response
2016-03-06 02:50:03 -08:00
Fosco Marotto 3266d59fcc beforeSave changes should propagate to the response 2016-03-06 02:16:17 -08:00
Marco129 908a4eb643 Fix delete relation field when _Join collection not exist 2016-03-06 17:22:36 +08:00
Fosco Marotto 2d4c08c5a3 Test empty authData block on login for #413 2016-03-06 01:03:51 -08:00
Fosco Marotto 4ee9cb754c Fix for related query on non-existing column 2016-03-06 00:34:44 -08:00
Drew b3a52fb52c Merge pull request #859 from igorshubovych/patch-1
Fix Markdown in .github/ISSUE_TEMPLATE.md
2016-03-05 18:51:06 -08:00
Igor Shubovych 91eeca3500 Fix Markdown format: make checkboxes visible
https://youtu.be/ziSkbxWwDIQ?t=26
2016-03-06 00:15:32 +02:00
Drew 1a723c648a Merge pull request #857 from tobernguyen/master
Fix create wrong _Session for Facebook login
2016-03-05 11:52:39 -08:00
Long Nguyen fcc8032e4c Merge branch 'master' into master 2016-03-06 01:37:12 +07:00
Long Nguyen 4f643d970a Fix create wrong _Session for Facebook login 2016-03-06 01:32:50 +07:00
Drew 930e8422b5 Merge pull request #846 from aneeshd16/patch-1
Modified the npm dev script to support Windows
2016-03-04 22:09:28 -08:00
Aneesh Devasthale 7cc059973b Modified the npm dev script to support Windows
Windows does not support shebangs/hashbangs. Added the node command to run the bin/dev script. Extension of #831
2016-03-05 10:47:27 +05:30
Florent Vilmart 17bc79b372 Improves tests, ensure unicity of roleIds 2016-03-04 22:52:27 -05:00
Florent Vilmart c9f8453171 Fix reversed roles lookup 2016-03-04 22:52:26 -05:00
Florent Vilmart fb9f784a87 Merge pull request #843 from drew-gross/improve-tests
Fix leak warnings in tests, use mongodb-runner from node_modules
2016-03-04 22:50:17 -05:00
Nikita Lutsenko 172da3aaa3 Move HooksController to use MongoCollection instead of direct Mongo access. 2016-03-04 19:21:34 -08:00
Drew Gross e074a922fb Fix leak warnings in tests, use mongodb-runner from node_modules 2016-03-04 19:00:12 -08:00
Drew c6ae64bdbe Merge pull request #839 from ParsePlatform/flovilmart.includePublicPages
Adds public_html and views for packaging
2016-03-04 18:25:50 -08:00
Florent Vilmart a44b1d9f76 Improves documentation, add loading tests 2016-03-04 18:09:54 -05:00
Florent Vilmart 069605e9c3 Improves loading of Push Adapter, fix loading of S3Adapter
- Adds environment variables to configure S3Adapter
2016-03-04 18:08:35 -05:00
Florent Vilmart 2c844a11b9 Adds public_html and views for packaging 2016-03-04 17:01:14 -05:00
Drew 3e6c53c3e4 Merge pull request #831 from ParsePlatform/flovilmart.fixWindowsTests
Better support for windows builds
2016-03-04 11:40:31 -08:00
Florent Vilmart ee38ebc4a3 Removes shebang for windows 2016-03-04 13:44:56 -05:00
Florent Vilmart 1a17d16dc9 Better support for windows builds 2016-03-04 13:44:56 -05:00
Nikita Lutsenko ab11ef158f Merge pull request #826 from ParsePlatform/nlutsenko.schema
Convert Schema.js to ES6 class.
2016-03-04 08:54:59 -08:00
Drew 438e802a6a Merge pull request #828 from Marco129/fix-update-system-schema
Fix add field to system schema
2016-03-04 08:53:23 -08:00
Marco129 8814e1f3b5 Fix add field to system schema 2016-03-04 20:44:21 +08:00
Nikita Lutsenko 295d361725 Convert Schema.js to ES6 class. 2016-03-04 01:05:49 -08:00
Mike McDonald 5b8ad9d4c6 No more .DS_Store 2016-03-03 22:50:19 -08:00
Mike McDonald 1dc346715d Merge branch 'master' of https://github.com/ParsePlatform/parse-server into mcdonald-gcs-adapter
Get GCSAdapter up to snuff with FilesController + FilesControllerTestFactory

* 'master' of https://github.com/ParsePlatform/parse-server: (102 commits)
  Remove duplicated instructions
  Release and Changelog for 2.1.4
  fixes missing coverage with sh script
  Fix update system schema
  Adds optional COVERAGE
  Allows to pass no where in $select clause
  Sanitize objectId in
  Fix delete schema when actual collection does not exist
  Fix replace query overwrite the existing query object.
  Fix create system class with relation/pointer
  Use throws syntax for errors in SchemasRouter.
  Completely migrate SchemasRouter to new MongoCollection API.
  Add tests that verify installationId in Cloud Code triggers.
  Propagate installationId in all Cloud Code triggers.
  Add test
  expiresAt should be a Date, not a string. Fixes #776
  Fix missing 'let/var' in OneSignalPushAdapter.spec.
  Don't run any afterSave hooks if none are registered.
  Fix : remove query count limit
  Flatten custom operations in request.object in afterSave hooks.
  ...
2016-03-03 22:36:25 -08:00
Florent Vilmart e3d26e2b4e Merge pull request #816 from ParsePlatform/readme-dupe
Remove duplicated instructions
2016-03-03 23:11:25 -05:00
Fosco Marotto 33ac89569f Merge pull request #780 from ParsePlatform/hramos-remove-gcp-deploy
Remove Deploy to GCP Button
2016-03-03 17:51:27 -08:00
Nikita Lutsenko 9a7264100c Merge pull request #794 from ParsePlatform/nlutsenko.schemasRouter
Completely migrate SchemasRouter to new MongoCollection API.
2016-03-03 14:55:54 -08:00
Héctor Ramos 8363122c65 Remove duplicated instructions 2016-03-03 13:14:33 -08:00
Fosco Marotto 06fb8d451a Merge pull request #810 from drew-gross/changelog-2.1.4
Release and Changelog for 2.1.4
2016-03-03 12:07:07 -08:00
Drew b6009bb393 Merge pull request #811 from Marco129/fix-update-system-schema
Fix update system schema
2016-03-03 11:51:03 -08:00
Drew Gross 4d7c87b104 Release and Changelog for 2.1.4 2016-03-03 11:45:07 -08:00
Florent Vilmart 2d509cdf1c Merge pull request #813 from ParsePlatform/flovilmart.fasterTests
fixes missing coverage with sh script
2016-03-03 14:42:55 -05:00
Florent Vilmart ec8529ada7 fixes missing coverage with sh script 2016-03-03 14:32:15 -05:00
Marco129 50735c4cbb Fix update system schema 2016-03-04 03:09:49 +08:00
Drew b301ea9a9c Merge pull request #802 from Marco129/fix-delete-schemas
Fix delete schema when actual collection does not exist
2016-03-03 10:44:35 -08:00
Drew a44e371f40 Merge pull request #797 from Marco129/fix-schema-api
Fix create system class with relation/pointer
2016-03-03 10:36:08 -08:00
Florent Vilmart dbcbf60816 Merge pull request #807 from ParsePlatform/flovilmart.fasterTests
Adds optional coverage for faster tests
2016-03-03 13:16:35 -05:00
Florent Vilmart 0e39b3b0e7 Adds optional COVERAGE
renames COVERAGE to CODE_COVERAGE

Updates env in .travis.yaml
2016-03-03 13:08:59 -05:00
Drew 3b4faaaf95 Merge pull request #799 from carmenlau/query-fix
Fix using query.notContainedIn and query.doesNotMatchQuery at the same time, notContainedIn will be ignored problem
2016-03-03 09:50:06 -08:00
Drew 187f29c2a6 Merge pull request #809 from ParsePlatform/flovilmart.objectIdInSanitation
Sanitize objectId $in management, fix $select bug from iOS
2016-03-03 09:48:44 -08:00
Florent Vilmart e64b6860c1 Allows to pass no where in $select clause
- This is causing a bug for iOS SDK when no query constraints are set
2016-03-03 11:40:57 -05:00
Florent Vilmart 907a05c57a Sanitize objectId in
- if objectId is set in query, move it to $in array
- refactors to addInObjectIdsIds
2016-03-03 10:58:09 -05:00
Marco129 f157538bfd Fix delete schema when actual collection does not exist 2016-03-03 18:46:47 +08:00
Carmen 6973de7910 Fix replace query overwrite the existing query object. 2016-03-03 16:43:44 +08:00
Marco129 90a1e46905 Fix create system class with relation/pointer 2016-03-03 16:37:30 +08:00
Nikita Lutsenko 99cb05ea1e Use throws syntax for errors in SchemasRouter. 2016-03-02 23:48:12 -08:00
Nikita Lutsenko 2afebf955f Completely migrate SchemasRouter to new MongoCollection API. 2016-03-02 21:34:40 -08:00
Nikita Lutsenko c6406b9e1a Merge pull request #790 from drew-gross/expiresAt-fix
Expires at fix
2016-03-02 21:22:28 -08:00
Drew 9bc1a3556e Merge pull request #791 from ParsePlatform/nlutsenko.triggers.installationId
Propagate installationId to all Cloud Code triggers.
2016-03-02 21:21:27 -08:00
Nikita Lutsenko edc7720660 Add tests that verify installationId in Cloud Code triggers. 2016-03-02 21:02:50 -08:00
Nikita Lutsenko c4fa3f0ee0 Propagate installationId in all Cloud Code triggers. 2016-03-02 21:02:50 -08:00
Drew Gross befcd453a4 Add test 2016-03-02 21:01:24 -08:00
Drew Gross df4022060e expiresAt should be a Date, not a string. Fixes #776 2016-03-02 20:32:06 -08:00
Nikita Lutsenko 1053adf01d Merge pull request #784 from ParsePlatform/nlutsenko.afterSave
Flatten custom operations in request.object in afterSave hooks.
2016-03-02 20:15:59 -08:00
Drew 2cc9600604 Merge pull request #785 from flessard/master
Fix : remove query count limit
2016-03-02 20:10:22 -08:00
Florent Vilmart cd4c4169d5 Merge pull request #777 from ParsePlatform/flovilmart.removeKeysWhenIncludeIsMissing
Fixes mismatching behavior in including keys
2016-03-02 22:16:12 -05:00
Florent Vilmart dac71019b6 Merge pull request #769 from ParsePlatform/flovilmart.fixFailingQueries
Adds support for multiple $in, pointer / relation queries on $or
2016-03-02 22:15:37 -05:00
Nikita Lutsenko 358a7ae7f3 Fix missing 'let/var' in OneSignalPushAdapter.spec. 2016-03-02 18:43:15 -08:00
Nikita Lutsenko c4aac335e0 Don't run any afterSave hooks if none are registered. 2016-03-02 18:38:06 -08:00
Francis Lessard f3b7138269 Fix : remove query count limit
Remove the limit on query count. By default the limit is 100. If you
try to get the count of a collection and the collection has more than
100 rows, the result is always 100.
2016-03-02 21:34:17 -05:00
Nikita Lutsenko b778b314fb Flatten custom operations in request.object in afterSave hooks. 2016-03-02 18:25:27 -08:00
Florent Vilmart d8d374379d Merge pull request #740 from ParsePlatform/flovilmart.badgeIncrement
Adds support for badging on iOS
2016-03-02 21:17:16 -05:00
Florent Vilmart ee3b37d4a2 Adds support for badging on iOS 2016-03-02 20:51:26 -05:00
Florent Vilmart d872f52eff backbone style is BAD! 2016-03-02 20:37:45 -05:00
Héctor Ramos 747482227d Remove Deploy to GCP Button
The GCP button links to a step by step guide, and is not really a quick-start way to deploy the app.

It's fine to add it back in the `parse-server-example` repo itself, as well as it include it in our community links.
2016-03-02 17:29:11 -08:00
Florent Vilmart e477fadec4 Merge pull request #778 from ParsePlatform/flovilmart.typeChecking
Nit for Object type
2016-03-02 20:27:26 -05:00
Florent Vilmart 5f9a8d87e5 Nit for Object type 2016-03-02 20:00:26 -05:00
Drew d70e87f141 Merge pull request #748 from drew-gross/touch-up-features-endpoint
Touch up features endpoint
2016-03-02 16:43:12 -08:00
Florent Vilmart 6ddc77601c Fixes mismatching behavior in including keys
- When including a key, parse.com would set to undefined all not found pointer, not parse-server
2016-03-02 19:38:42 -05:00
Drew Gross ccc2a1a03f Fix tests again 2016-03-02 16:34:07 -08:00
Drew Gross a56fe0f7e6 Fix tests 2016-03-02 16:16:16 -08:00
Drew Gross 36202badf3 Return parse server version 2016-03-02 16:16:16 -08:00
Drew Gross 81519852d1 Report Server Version so Dashboard can consume it 2016-03-02 16:16:16 -08:00
Drew Gross 5219e0b1d8 Touch up features endpoint 2016-03-02 16:16:16 -08:00
Florent Vilmart 1bfee45f64 Merge pull request #773 from ParsePlatform/flovilmart.fixHttpRequest
Graceful fails on httpRequest
2016-03-02 18:43:36 -05:00
Florent Vilmart 8eff44410f Graceful fails on httpRequest 2016-03-02 18:31:39 -05:00
Drew 4b7447e228 Merge pull request #710 from sdf611097/typeChecking
Add __type checking
2016-03-02 14:57:19 -08:00
Nikita Lutsenko 818fe492d7 Merge pull request #774 from ParsePlatform/fosco.install-fix
Fix an installation deduplication bug
2016-03-02 14:36:13 -08:00
Nikita Lutsenko bb80e5d69f Merge pull request #759 from ParsePlatform/nlutsenko.storage.findAndDoMagic
Add findOneAndDelete, findOneAndModify to MongoCollection, move most of usages to it.
2016-03-02 14:36:01 -08:00
Fosco Marotto bfafcd4e87 Fix an installation deduplication bug 2016-03-02 14:14:12 -08:00
Florent Vilmart 43f014a47d nits 2016-03-02 15:16:48 -05:00
Florent Vilmart 3629c40036 Adds support for or queries on pointer and relations 2016-03-02 14:33:51 -05:00
Florent Vilmart 27815b18aa Adds support for multiple $in 2016-03-02 12:01:49 -05:00
Florent Vilmart c7503fc654 Merge pull request #735 from sdf611097/invalidRole
_Role object must specified name and ACL
2016-03-02 09:39:44 -05:00
Florent Vilmart 4e312673c7 Merge pull request #746 from ParsePlatform/flovilmart.logsEndpoint
adds scriptlog endpoint
2016-03-02 09:38:49 -05:00
Florent Vilmart 63dc64004d Removes /logs endpoint 2016-03-02 09:15:44 -05:00
Florent Vilmart 8e034dd9b0 adds scriptlog endpoint 2016-03-02 09:09:54 -05:00
Florent Vilmart 9cc7bd064c Merge pull request #751 from ParsePlatform/flovilmart.hotfixCLI
Fix error preventing starting parse-server from CLI with a config file
2016-03-02 09:07:30 -05:00
jim1_lin 789dc48f47 remove redundant log 2016-03-02 21:09:20 +08:00
jim1_lin 36aa5935b1 change error msg 2016-03-02 16:50:00 +08:00
Nikita Lutsenko 4049ce4102 Move DatabaseController to use new findOneAndUpdate. 2016-03-02 00:29:20 -08:00
Nikita Lutsenko 6d7813be4a Move UserController to use adaptiveCollection for findOneAndUpdate. 2016-03-02 00:29:20 -08:00
Nikita Lutsenko 244009923f Add findOneAndUpdate to MongoCollection. 2016-03-02 00:29:18 -08:00
Nikita Lutsenko e39286d88b Implement findAndDelete in MongoCollection and move SchemasRouter to it. 2016-03-02 00:28:37 -08:00
jim1_lin a909ab71e3 ACL must be specified in _Role 2016-03-02 16:08:39 +08:00
Nikita Lutsenko 4bd163b790 Remove direct mongo collection access from UserController.checkResetTokenValidity. 2016-03-02 00:04:44 -08:00
Nikita Lutsenko 4fe670efaf Merge pull request #752 from ParsePlatform/nlutsenko.adapter.collection
Add MongoCollection wrapper and move few basic uses of collection to it.
2016-03-01 23:05:09 -08:00
Nikita Lutsenko 63f7468ce7 Merge pull request #753 from ParsePlatform/nlutsenko.middleware.routers
Use shared middleware to enforce masterkey security on logs/global config/hooks APIs.
2016-03-01 22:44:04 -08:00
Nikita Lutsenko 9538a7dab5 Make parts of SchemasRouter use adaptiveCollection. 2016-03-01 22:36:23 -08:00
Nikita Lutsenko abde9484ce Use adaptiveCollection for main find/count inside DatabaseController. 2016-03-01 22:36:23 -08:00
Nikita Lutsenko 49994b6e87 Add MongoCollection and adaptiveCollection abstraction to MongoAdapter. 2016-03-01 22:36:23 -08:00
Nikita Lutsenko 17235b576b Use shared middleware to enforce master key on logs API. 2016-03-01 20:42:41 -08:00
Nikita Lutsenko 806800c6fb Use shared middleware to enforce master key on global config update API. 2016-03-01 20:42:41 -08:00
Nikita Lutsenko dacc22de42 Use shared middleware to enforce master key on hooks API. 2016-03-01 20:29:55 -08:00
jim1_lin 5f9b5d54f3 move check from classRouter to Schema.js 2016-03-02 12:21:18 +08:00
Florent Vilmart a0150a743f Fix error preventing starting parse-server from CLI with a config file 2016-03-01 22:33:58 -05:00
jim1_lin 4054c247ec Add test, JavascriptSDK 1.7.1 rest format not as expected
JavascriptSDK 1.7.1 use serverUrl/classes/_Role directly.
So move validation from RolesRouter to ClassesRouter.
2016-03-02 11:13:24 +08:00
Nikita Lutsenko 92e51ab4d3 Merge pull request #747 from ParsePlatform/nlutsenko.schema.middleware
Add promise-based master-key enforce middleware and move SchemasRouter, FeaturesRouter to use it.
2016-03-01 16:23:19 -08:00
Nikita Lutsenko fb4a2524b1 Cleanup and use masterkey middleware in FeaturesRouter. 2016-03-01 16:16:07 -08:00
Nikita Lutsenko e58c935f22 Use middleware instead of custom checks inside SchemasRouter. 2016-03-01 16:16:07 -08:00
Nikita Lutsenko 8ce0bd84fb Add promise-based master-key only middleware. 2016-03-01 16:16:07 -08:00
Florent Vilmart a79fb3a64c Merge pull request #742 from ParsePlatform/flovilmart.hotfixFilesAdapter
documents createFile
2016-03-01 18:34:01 -05:00
Drew 72e6eae727 Merge pull request #739 from ParsePlatform/peterjs.featuresEndpoint
Features Endpoint for Dashboard.
2016-03-01 15:08:15 -08:00
Florent Vilmart 750a5794d3 Merge pull request #744 from ParsePlatform/flovilmart.betterPromiseRouter
Better promise router
2016-03-01 17:21:19 -05:00
Florent Vilmart 9601ca4fca Better promise router 2016-03-01 17:11:23 -05:00
Florent Vilmart ad2e3c9b09 documents createFile 2016-03-01 15:46:36 -05:00
Peter Shin 66eaf6c6ef Features Endpoint for Dashboard. 2016-03-01 12:21:57 -08:00
Florent Vilmart 0e58c8d867 Merge pull request #738 from ParsePlatform/flovilmart.s3Improvements
S3 Improvements
2016-03-01 15:08:30 -05:00
Florent Vilmart 7257ee858b Moves some logic from FilesRouter to FilesController for content-type and filename 2016-03-01 10:14:03 -05:00
Florent Vilmart 78d380df72 Adds content type support in S3 2016-03-01 09:28:17 -05:00
Florent Vilmart 9287afc3c2 refactors filesAdapter tests in factories 2016-03-01 09:02:36 -05:00
jim1_lin 3889223b10 Avoid Role object without name 2016-03-01 20:30:37 +08:00
Nikita Lutsenko 0b990b671a Merge pull request #729 from ParsePlatform/nlutsenko.decouple.schema
Decouple and remove direct mongo access from Schema/SchemaRouter.
2016-02-29 23:53:01 -08:00
Nikita Lutsenko 382149b190 Merge pull request #733 from ParsePlatform/nlutsenko.decouple.files
Make GridStoreAdapter persist it's own connection and don't talk to config.database.
2016-02-29 23:52:49 -08:00
Nikita Lutsenko 63a534f31d Make GridStoreAdapter persist it's own connection and don't talk to config.database. 2016-02-29 21:00:04 -08:00
Nikita Lutsenko d1dbc1a035 Remove direct mongo access from schemas.spec.js. 2016-02-29 19:47:08 -08:00
Nikita Lutsenko 2733c0924b Remove direct mongo access from Schema.spec.js. 2016-02-29 19:47:08 -08:00
Nikita Lutsenko 6893895aea Remove direct mongo access from SchemaRouter.modify, Schema.deleteField. 2016-02-29 19:47:08 -08:00
Nikita Lutsenko 028ef2a7b2 Remove dependency on raw mongo from SchemaRouter.delete. 2016-02-29 19:47:08 -08:00
Florent Vilmart d7b1184b5e Merge pull request #627 from flovilmart/mail-adapter
Email Validation
2016-02-29 21:16:03 -05:00
Florent Vilmart 28d1a8afe4 Sends 404 when parseServerURL is not set on public pages
- throws when verifyEmail = true && publicServerURL not set
2016-02-29 20:51:13 -05:00
Florent Vilmart 6aa38ea8ca Improves validation of email parameters in Configuration 2016-02-29 19:26:50 -05:00
Florent Vilmart 2183b0be82 Allows very simple mail adapters
- Fix nasty bug when updating users email and sending verification
2016-02-29 18:58:35 -05:00
Florent Vilmart 3ecaa0aa4b Sends verification email upon set and update email
- nits
2016-02-29 18:58:34 -05:00
Florent Vilmart 91d9724182 Adds reset password logic 2016-02-29 18:55:15 -05:00
Florent Vilmart f3bb2c99e0 Refactor and advancements
- Drops mailController, centralized in UserController
- Adds views folder for change_password
- Improves PromiseRouter to support text results
- Improves PromiseRouter to support empty responses for redirects
- Adds options to AdaptableController
- UsersController gracefully fails when no adapter is set
- Refactors GlobalConfig into same style for Routers
2016-02-29 18:55:14 -05:00
Florent Vilmart 7dd765256c Refactors verify_email, adds public html 2016-02-29 18:52:11 -05:00
Florent Vilmart 0b307bc22f Improves AdapterLoader, enforces configuraiton on Adapters 2016-02-29 18:52:10 -05:00
Drew Gross 8dc37b9d30 Exploring the interface of a mail adapter
Add some tests and demonstrate the adapter loading interface
2016-02-29 18:52:10 -05:00
Fosco Marotto d9f1e00345 Merge pull request #702 from simonbengtsson/before-save-login
Don't trigger beforeSave when logging in with a provider
2016-02-29 14:40:22 -08:00
Fosco Marotto 04491fcb20 Merge pull request #673 from stephentuso/installation-handling-fix
Handle duplicate android device tokens correctly
2016-02-29 14:37:24 -08:00
Fosco Marotto 46da50f2b3 Merge pull request #689 from ParsePlatform/flovilmart.rolesHierarchy
Recursive lookup for roles
2016-02-29 14:35:04 -08:00
Nikita Lutsenko e9699b64a5 Merge pull request #698 from ParsePlatform/nlutsenko.database.controller
Rename ExportAdapter to DatabaseController, start splitting Mongo specific logic.
2016-02-29 14:09:03 -08:00
Nikita Lutsenko eb892830e6 Move and cleanup getting collections into MongoStorageAdapter. 2016-02-29 12:51:43 -08:00
Nikita Lutsenko 7215300c1e Move Mongo database property directly to mongo adapter. 2016-02-29 12:50:47 -08:00
Nikita Lutsenko 997da898eb Split mongodb connection creation from DatabaseController. 2016-02-29 12:50:47 -08:00
Nikita Lutsenko d78c2746e9 Rename ExportAdapter to DatabaseController. 2016-02-29 12:50:47 -08:00
Nikita Lutsenko 10efd5d38d Merge pull request #694 from ParsePlatform/nlutsenko.cache
Unify and cleanup cache.js.
2016-02-29 12:49:04 -08:00
Drew 888354154d Merge pull request #714 from ParsePlatform/flovilmart.echoKeysTests
Adds test that ensures the keys are properly set when using cloudcode
2016-02-28 12:35:32 -08:00
Florent Vilmart e43c471a7e Adds test that ensures the keys are properly set when using cloudcode 2016-02-28 12:38:36 -05:00
Jim Lin 430dd7944e Add test cases for __type checking 2016-02-28 16:08:26 +08:00
Mike McDonald 3e41a21e27 x-ing out the test suite for CI buildability. 2016-02-27 22:13:20 -08:00
Jim Lin f60c6af5e7 Fix bug to __type checking 2016-02-28 14:02:40 +08:00
Jim Lin 88b14a914a url can & should be constructed during read 2016-02-28 14:02:40 +08:00
jim1_lin ae9f196ec7 add Bytes to check 2016-02-28 14:02:40 +08:00
jim1_lin dec95ca4fa Add check of special type 2016-02-28 14:02:40 +08:00
jim1_lin d067c0ba61 add invalid __type check 2016-02-28 14:02:40 +08:00
Mike McDonald 360cc3461d Added tests! Note that tests won't run without a GCP Project, key, and GCS bucket 2016-02-27 21:27:32 -08:00
Mike McDonald deafb680ae Removed orphaned "options" 2016-02-27 16:47:26 -08:00
Mike McDonald 2520e7b6c6 Initial commit of Google Cloud Storage File Adapter 2016-02-27 16:33:34 -08:00
Drew bd89338b39 Merge pull request #696 from mcdonamp/gcp-logo
Adding the GCP logo and quickstart
2016-02-27 12:04:39 -08:00
Drew 97d6e9d1a8 Merge pull request #695 from Marco129/client-class-creation
Allow create system class even allowClientClassCreation option is false
2016-02-27 11:02:34 -08:00
Simon Bengtsson 4eb9bd442f Adds test that makes sure beforeSave is not called on User when logging in with a provider 2016-02-27 15:57:44 +01:00
Simon Bengtsson 6cafd46d06 Validate authData before triggering beforeSave 2016-02-27 15:54:43 +01:00
Mike McDonald 310650830e Adding the GCP logo and quickstart 2016-02-27 00:23:17 -08:00
Marco129 c359d0fb5f Allow create system class even allowClientClassCreation option is false 2016-02-27 15:37:34 +08:00
Nikita Lutsenko 768a781f98 Fix wrong order of resetting data in test helper. 2016-02-26 22:48:53 -08:00
Nikita Lutsenko d30c3e90df Remove useless method in index.js. 2016-02-26 22:48:53 -08:00
Nikita Lutsenko daa5f11122 Remove 'database' field from request and make all database requests go through config. 2016-02-26 22:48:53 -08:00
Nikita Lutsenko 49a4b74f7c Fix missing return type on requiredParameter.js. 2016-02-26 22:48:53 -08:00
Nikita Lutsenko 9bc636dc5c Cleanup, remove unusued methods and unify cache.js. 2016-02-26 22:48:53 -08:00
Nikita Lutsenko db9b72b147 Merge pull request #693 from ParsePlatform/nlutsenko.collectionPrefix
Remove dependency from DatabaseAdapter on cache.js.
2016-02-26 21:20:46 -08:00
Nikita Lutsenko 0cf3f56b00 Remove dependency from DatabaseAdapter on cache.js. 2016-02-26 20:46:49 -08:00
stephentuso 02b56def6d Merge branch 'android-installation-duplicate-token-test' into installation-handling-fix 2016-02-26 21:10:14 -05:00
stephentuso aee968a853 Add duplicate device token test 2016-02-26 21:05:46 -05:00
stephentuso c9d4f7693d Should pass tests now 2016-02-26 19:42:47 -05:00
Peter J. Shin 49b01c961b Merge pull request #690 from ParsePlatform/peterjs.issueTemplate
Create ISSUE_TEMPLATE.md
2016-02-26 13:58:51 -08:00
Drew ca9b750993 Merge pull request #684 from Marco129/client-class-creation
Add allowClientClassCreation option
2016-02-26 11:46:50 -08:00
Drew 9142dec5fd Merge pull request #679 from ParsePlatform/fosco.empty-authData
Fix for #413 - support empty authData on signup
2016-02-26 11:17:25 -08:00
Peter J. Shin cdb82c20bb Update ISSUE_TEMPLATE.md 2016-02-26 10:56:45 -08:00
Peter J. Shin dd771d429e Create ISSUE_TEMPLATE.md 2016-02-26 10:50:27 -08:00
Florent Vilmart 753bead4ac Recursive lookup for roles 2016-02-26 13:35:56 -05:00
Fosco Marotto 34a621b279 Merge pull request #682 from ParsePlatform/flovilmart.fixKeysChecking
Improves key matching algorithm
2016-02-26 07:39:59 -08:00
Florent Vilmart 4e5cc1feb0 Fixes handing of no keys set in config 2016-02-26 10:22:05 -05:00
Florent Vilmart ea2de87b1a Improves key matching algorithm 2016-02-26 10:18:16 -05:00
Marco129 97489106b4 Add allowClientClassCreation option 2016-02-26 23:08:20 +08:00
Fosco Marotto 57c936485e Merge pull request #680 from ParsePlatform/fosco.jssdk
Fix for JS SDK needing a key
2016-02-26 06:48:19 -08:00
Fosco Marotto 6a17eca69c Fix for JS SDK needing a key 2016-02-26 06:25:37 -08:00
Fosco Marotto 1b83d9c621 Fix for #413 - support empty authData on signup 2016-02-26 05:05:15 -08:00
Fosco Marotto 791c5512aa Merge pull request #567 from IlyaDiallo/master
Accept subdocuments keys ("object.subobject"), to allow atomic updates of an object field.
2016-02-26 02:05:03 -08:00
stephentuso 7ca8836768 Treat objectId and installationId matches the same when handling Installation writes 2016-02-26 00:55:36 -05:00
Nikita Lutsenko 3b1165b686 Merge pull request #670 from ParsePlatform/nlutsenko.strings
Use constants instead of magic strings for trigger names.
2016-02-25 20:45:25 -08:00
Nikita Lutsenko 54e46d4157 Merge pull request #666 from ParsePlatform/nlutsenko.files.url
Remove 'url' as a required field on Files when they are stored on object.
2016-02-25 20:45:17 -08:00
Nikita Lutsenko 5e4cecd94e Use constants instead of magic strings for trigger names. 2016-02-25 20:01:52 -08:00
Nikita Lutsenko a96cb6b31d Remove 'url' as a required field on Files when they are stored on object. 2016-02-25 19:31:55 -08:00
Drew 1945313794 Merge pull request #663 from ParsePlatform/flovilmart.hotfix#659
sets defaults value to options for Type error in CLI
2016-02-25 17:15:39 -08:00
Drew 839f6b795c Merge pull request #652 from ParsePlatform/flovilmart.fixCloudCodeQueryProblem
Generates a javascriptKey if not passed for CloudCode
2016-02-25 16:28:16 -08:00
Florent Vilmart fa729c5c18 sets defaults value to options 2016-02-25 19:07:17 -05:00
Florent Vilmart 98c65062e5 Generates a javascriptKey if not passed for CloudCode 2016-02-25 14:15:09 -05:00
Drew e5b1d50343 Merge pull request #500 from flovilmart/hooks-API
Adds Hooks API
2016-02-25 11:03:54 -08:00
Florent Vilmart 3c6141576f Fixes 2016-02-25 13:32:19 -05:00
Florent Vilmart c4abd1e267 cache as ES6 2016-02-25 08:10:08 -05:00
Florent Vilmart c0714b9b9d Shows --port option in README 2016-02-25 08:10:07 -05:00
Florent Vilmart 9ac7a52e40 Adds Hooks API
Adds Parse.Hooks.js in src/cloud-code/Parse.Hooks.js
Moves Cloud code related functions in src/cloud-code
2016-02-25 08:10:07 -05:00
Drew a7262dafd8 Merge pull request #603 from flovilmart/proper-cli
Proper CLI with commander
2016-02-24 20:51:23 -08:00
Drew e58aa54e31 Merge pull request #640 from drew-gross/allow-master-key
Allow master key headers in cross origin requests.
2016-02-24 17:39:13 -08:00
Drew Gross c867e37559 Allow master key headers 2016-02-24 17:26:50 -08:00
Fosco Marotto 535853311f Merge pull request #634 from drew-gross/2.1.3
Changelog and package.json for 2.1.3
2016-02-24 16:47:21 -08:00
Florent Vilmart db5338abaa Infers serverURL based on mountPath and port 2016-02-24 19:17:50 -05:00
Florent Vilmart fe11fb54fc Stupid ES6 imports... 2016-02-24 19:17:50 -05:00
Florent Vilmart f58befd7bb ES6-ify 2016-02-24 19:17:50 -05:00
Florent Vilmart 3ca1e9f79e Adds maxUploadSize 2016-02-24 19:17:50 -05:00
Florent Vilmart 81f5c682d4 Updates README, makes serverURL required
- prints help when misconfigured
- prints missing appId, masterKey, serverURL
2016-02-24 19:17:50 -05:00
Florent Vilmart 30aafd2c62 Adds proper tests 2016-02-24 19:17:49 -05:00
Florent Vilmart ab425d85b0 New CLI for parse-server 2016-02-24 19:17:49 -05:00
Nikita Lutsenko 7bbe1665f8 Merge pull request #637 from ParsePlatform/nlutsenko.files.expand
Fix missing file URL for short-circuited _User in RestWrite.
2016-02-24 15:57:12 -08:00
Nikita Lutsenko 970682a499 Merge pull request #636 from ParsePlatform/andrewi.ship_less
Move npmignore blacklist to package.json whitelist
2016-02-24 15:28:28 -08:00
Nikita Lutsenko 9d342a8aee Fix missing file URL for short-circuited _User in RestWrite. 2016-02-24 15:18:40 -08:00
Andrew Imm f3286f9d16 Move npmignore blacklist to package.json whitelist 2016-02-24 14:47:27 -08:00
Drew f2f42fe66d Update CHANGELOG.md 2016-02-24 13:33:17 -08:00
Nikita Lutsenko f461f2abb3 Merge pull request #632 from ParsePlatform/nlutsenko.users.login
Send a sessionToken back on /users/me.
2016-02-24 13:16:08 -08:00
Drew Gross e15f9dc8b1 Changelog and package.json for 2.1.3 2016-02-24 13:11:33 -08:00
Nikita Lutsenko bc90effd32 Send a sessionToken back on /users/me. 2016-02-24 12:54:04 -08:00
Peter J. Shin 5fae41183e Updating comments for loggerAdapter. 2016-02-24 11:15:26 -08:00
Nikita Lutsenko a41ca281f0 Merge pull request #619 from ParsePlatform/nlutsenko.triggers.inflate
Improve beforeSave triggers for newly created objects.
2016-02-24 10:27:23 -08:00
Nikita Lutsenko ae82f33118 Inflate all data as dirty for new ParseObject in beforeSave. 2016-02-24 01:24:16 -08:00
Nikita Lutsenko 74289f9ce5 Use saveJSON instead of full JSON to avoid expansion of pointers. 2016-02-24 01:24:16 -08:00
Nikita Lutsenko 39dce71e4f Add a test to validate pointer mutation in beforeSave hooks. 2016-02-24 01:24:16 -08:00
Nikita Lutsenko 7a14695da8 Restrict triggers on _Session class. 2016-02-24 01:24:16 -08:00
Nikita Lutsenko b477d16992 Do not do any setup for beforeSave trigger if none is set for this className. 2016-02-24 01:24:15 -08:00
Nikita Lutsenko 1ae61c92c8 Merge pull request #614 from ParsePlatform/nlutsenko.beforeSave.dirty
Fix dirtyKeys() and dirty(key:) on beforeSave when updating objects.
2016-02-23 22:35:57 -08:00
Drew 63a7e854fd Merge pull request #608 from drew-gross/throw-no-server-url
Require a server URL
2016-02-23 22:11:16 -08:00
Nikita Lutsenko 24a25d14b3 Unify all beforeSave with custom hooks into a single describe(), reset the global state on before/after test. 2016-02-23 22:01:12 -08:00
Nikita Lutsenko 69efeac1a3 Enable strict mode in ParseAPI.spec.js. 2016-02-23 22:00:48 -08:00
Nikita Lutsenko 4643be7086 Remove duplicate import in helper.js. 2016-02-23 22:00:25 -08:00
Drew Gross 2dc080fd00 Require a server URL 2016-02-23 21:51:04 -08:00
Nikita Lutsenko 14cc82a057 Fix dirtyKeys() and dirty(key:) on beforeSave when updating objects. 2016-02-23 21:50:17 -08:00
Drew 531e2ffea8 Merge pull request #604 from flovilmart/adds-max-file-size-option
Adds maxUploadSize option
2016-02-23 18:33:25 -08:00
Drew b1c2d9da6a Merge pull request #576 from flovilmart/httpRequest-tests
improves coverage and API of httpRequest
2016-02-23 13:02:48 -08:00
Florent Vilmart 61e8f2aeea Adds maxUploadSize option 2016-02-23 11:49:21 -05:00
Florent Vilmart b6223f598c Adds support for application/x-www-form-urlencoded
- Now the body encoding is inferred on the headers as supposed
2016-02-23 08:17:48 -05:00
Nikita Lutsenko 1f707a97d3 Merge pull request #587 from ParsePlatform/nlutsenko.cc.test
Do not pass objectId, updatedAt, createdAt to beforeSave hooks on object create.
2016-02-22 22:13:51 -08:00
Drew 18d7a7a2af Merge pull request #586 from ParsePlatform/fosco.emptyACL
Fixed ACL creation for #504.
2016-02-22 21:57:31 -08:00
Nikita Lutsenko be693fb736 Do not pass objectId, updatedAt, createdAt to beforeSave hooks on object create. 2016-02-22 19:33:32 -08:00
Nikita Lutsenko 17e5686efd Move test-only Cloud Code into spec subfolder. 2016-02-22 19:33:29 -08:00
Fosco Marotto 5755126525 Fixed ACL creation for #504. 2016-02-22 17:33:11 -08:00
Drew 1f40c0a7cd Merge pull request #584 from flovilmart/hotfix-pushadapters
hotfix for #549
2016-02-22 16:45:52 -08:00
Drew 017863fe7a Merge pull request #582 from IlyaDiallo/510
#510 Detect when the port you try to run the server on is already in use
2016-02-22 16:29:52 -08:00
Florent Vilmart 48dcfe37e7 Removes shared code in PushAdapter, replaces AdapterLoader.load by loadAdapter 2016-02-22 18:31:10 -05:00
Nikita Lutsenko 8ce8e2bbe0 Merge pull request #579 from ParsePlatform/nlutsenko.files.contentType
Allow uploading files without Content-Type.
2016-02-22 14:44:38 -08:00
Ilya Diallo 24824757a7 #510 Detect when the port you are trying to run the server on is already in use. 2016-02-22 23:38:29 +01:00
Héctor Ramos 3f22d060eb Merge pull request #577 from ParsePlatform/readme
Move advanced configuration docs to the Parse Server Guide
2016-02-22 14:34:12 -08:00
Drew 57bfbfc76d Merge pull request #574 from ParsePlatform/fosco.tombstone
Added _tombstone as an ignored key in untransformObject
2016-02-22 14:13:07 -08:00
Nikita Lutsenko 966d10ca43 Allow uploading files without content-type. 2016-02-22 14:07:58 -08:00
Ilya Diallo 193f368686 #510 Detect when the port you are trying to run the server on is already in use. (reverted from commit 7da7c43fce) 2016-02-22 22:45:00 +01:00
Ilya Diallo 7da7c43fce #510 Detect when the port you are trying to run the server on is already in use. 2016-02-22 22:40:01 +01:00
Héctor Ramos 303388d585 Remove examples/ directory 2016-02-22 13:39:57 -08:00
Héctor Ramos 0fe2c64288 Additional README organization. 2016-02-22 13:34:26 -08:00
Florent Vilmart cc3d93f5d9 improves coverage and API of httpRequest 2016-02-22 15:47:30 -05:00
Nikita Lutsenko bbc3d36d51 Merge pull request #573 from ParsePlatform/nlutsenko.flakytest
Disable flaky test that fails on Travis-CI.
2016-02-22 12:18:48 -08:00
Fosco Marotto d7550710a5 Added _tombstone as an ignored key in untransformObject 2016-02-22 12:14:45 -08:00
Nikita Lutsenko fcf745ad1d Disable flaky test that fails on Travis-CI. 2016-02-22 12:01:55 -08:00
Nikita Lutsenko dddb6795cf Merge pull request #549 from flovilmart/refactors-adapter-controller
Improves Controller and Adapter relationship
2016-02-22 11:43:38 -08:00
Florent Vilmart 045caca946 private _adapter, ES6 setters and getters 2016-02-22 14:12:51 -05:00
Ilya Diallo 831c2ee3de Update the tests 2016-02-22 16:45:41 +01:00
Ilya Diallo 170dd40d3c Accept subdocuments keys ("object.subobject"), to allow atomic updates of an object field. 2016-02-22 12:57:18 +01:00
Florent Vilmart 23e55e941e Splits Adapter loading from AdaptableController
- Adds dynamic prototype conformance check upon setting adapter
- Throws when adapter is undefined, invalid in controller
2016-02-21 23:47:07 -05:00
Nikita Lutsenko 0dda59ff73 Merge pull request #544 from ParsePlatform/nlutsenko.flow
Add ability to use Flow type checking.
2016-02-21 18:15:46 -08:00
Florent Vilmart 33fa5a7b2a Adds setDefaultAdapter 2016-02-21 17:04:34 -05:00
Florent Vilmart d504681589 Improves Controller and Adapter relationship
- Controllers that have adapters are AdaptableControllers
- AdaptableController is responsible to instantiate the proper adapter if needed (string, function or BaseAdapter)
- BaseAdapter is the base class for adapters, allows skipping when passed directly to the controller
2016-02-21 17:04:34 -05:00
Nikita Lutsenko b774c568e0 Annotate cryptoUtils with Flow. 2016-02-21 01:52:53 -08:00
Nikita Lutsenko 4097f47b1d Add and enable Flow type checking. 2016-02-21 01:52:17 -08:00
Nikita Lutsenko bd548786ea Merge pull request #543 from ParsePlatform/nlutsenko.query
Fix passing parameters via URL query when querying for objects.
2016-02-21 00:41:06 -08:00
Nikita Lutsenko 04795ff165 Fix passing parameters via URL query when querying for objects. 2016-02-21 00:15:37 -08:00
Nikita Lutsenko fb1c6bb3fb Add babel-polyfill to index.js. 2016-02-20 23:45:54 -08:00
Nikita Lutsenko 9d9052f2e5 Merge pull request #542 from ParsePlatform/nlutsenko.dev
Fix relative path replacement in bin/dev.
2016-02-20 23:20:00 -08:00
Nikita Lutsenko 64d92daefa Fix relative path replacement in bin/dev. 2016-02-20 22:54:18 -08:00
Nikita Lutsenko 6f4a7a6ff2 Merge pull request #534 from flovilmart/refactor-to-routers
Refactors Controllers to split Controllers and Routers
2016-02-20 15:29:06 -08:00
Fosco Marotto d9b94c78ef Merge pull request #532 from flovilmart/sub-keys-column-names-restrictions
Fix problems with _keys in nested objects
2016-02-20 11:55:46 -08:00
Florent Vilmart fbb5e448e6 refactors LoggerController with LogsRouter 2016-02-20 13:52:36 -05:00
Florent Vilmart 305879a251 Refactors FilesController in FilesRouter and FilesController 2016-02-20 13:52:35 -05:00
Florent Vilmart b490688652 Splits Push handling in Router and Controller
- Improves tests and coverage, fix bugs
2016-02-20 10:51:56 -05:00
Florent Vilmart fbdd89c2d1 Fix problems with _keys in nested objects 2016-02-20 09:45:09 -05:00
Nikita Lutsenko eb0340585f Merge pull request #501 from flovilmart/app-agnostic-controllers
Refactors PushController and FilesController to support multiple apps
2016-02-19 21:25:56 -08:00
Florent Vilmart 3c4d5159b4 Refactors routers 2016-02-20 00:08:54 -05:00
Florent Vilmart 067946c66c Refactors PushController and FilesController to support multiple apps 2016-02-19 23:49:12 -05:00
Drew 8aaf8f2ff2 Merge pull request #498 from drew-gross/test-configurations
Add ability to test multiple server configurations
2016-02-19 19:28:30 -08:00
Drew Gross 5378de7398 Add server URL to tests 2016-02-19 19:00:43 -08:00
Nikita Lutsenko 78c5292bc3 Merge pull request #515 from flovilmart/receipt-validation
Adds receipt validation endpoint
2016-02-19 17:41:16 -08:00
Drew 941984f2e0 Merge pull request #390 from workslon/hotfix/access-control-allow-headers-response-header
Fix Access-Control-Allow-Headers to match exact headers
2016-02-19 14:42:13 -08:00
Fosco Marotto 01f4bcc3e3 Updating to 2.1.2 2016-02-19 13:04:49 -08:00
Drew Gross f8b1d9f303 Merge remote-tracking branch 'upstream/master' into test-configurations 2016-02-19 12:53:53 -08:00
Drew Gross 843f289163 Merge remote-tracking branch 'upstream/master' into test-configurations 2016-02-19 12:52:19 -08:00
Drew 8441dd2956 Merge pull request #518 from ParsePlatform/fosco.212
Updating to 2.1.2
2016-02-19 12:50:44 -08:00
Fosco Marotto 7db69ac8f2 Updating to 2.1.2 2016-02-19 12:10:00 -08:00
Fosco Marotto 3087b2121e Merge pull request #464 from simonbengtsson/master
Don't use https://api.parse.com/1 as default serverURL
2016-02-19 12:01:48 -08:00
Fosco Marotto 2afb28564e Merge pull request #374 from flessard/user-roles
Fix : User Roles not added to create, update or delete calls
2016-02-19 11:59:33 -08:00
Fosco Marotto 39f8143326 Merge pull request #410 from Marco129/query-constraints
Throw when query is encoded incorrectly
2016-02-19 11:56:01 -08:00
Florent Vilmart 6e55e59b86 Fix typo,
- Adds automatic downloadName to match original Parse API
2016-02-19 14:07:12 -05:00
Florent Vilmart 9c477907bf Adds receipt validation endpoint 2016-02-19 13:06:02 -05:00
Drew 5ed037f8a5 Merge pull request #512 from Joseph-LeGrice/_noBody_fix
Unity _noBody fix
2016-02-19 09:26:48 -08:00
Joseph Le Grice 3f6445d035 remove _noBody when req.body._noBody == false 2016-02-19 16:41:31 +00:00
Drew 02566f0906 Merge pull request #494 from ParsePlatform/fosco.qs
Updated readme with deploy buttons
2016-02-18 19:06:16 -08:00
Drew Gross c9dfac277f Add ability to test multiple server configurations 2016-02-18 18:44:04 -08:00
Fosco Marotto 4bdff73933 Merge pull request #492 from skinp/patp.s3-bucket
Remove default bucket in S3Adapter
2016-02-18 18:09:18 -08:00
Fosco Marotto 605f83c36a Updated readme with deploy buttons 2016-02-18 18:03:45 -08:00
Patrick Pelletier 5f757b0741 Remove default bucket in S3Adapter 2016-02-18 17:26:32 -08:00
Fosco Marotto 9678b7afee Merge pull request #486 from ParsePlatform/fosco.211
Updating to 2.1.1
2016-02-18 12:43:51 -08:00
Fosco Marotto a6dc76d52f Updating to 2.1.1 2016-02-18 11:28:10 -08:00
Francis Lessard 5b8aa22730 Merge remote-tracking branch 'ParsePlatform/master' into user-roles 2016-02-18 14:00:11 -05:00
Fosco Marotto c9431a8ff3 Merge pull request #485 from flovilmart/fix-README
Minor readme change
2016-02-18 10:58:07 -08:00
Florent Vilmart 4e453925f7 Minor readme change 2016-02-18 13:49:13 -05:00
Fosco Marotto ba68acfb0d Merge pull request #476 from sdf611097/master
ExportAdapter destroy nothing will cause change password failed
2016-02-18 10:44:36 -08:00
Drew 2e6173fb99 Merge pull request #474 from drew-gross/schemas-delete
Implement DELETE /schemas/:className
2016-02-18 10:41:43 -08:00
Fosco Marotto d1476ecabd Merge pull request #468 from codegefluester/ccv_pass_in_request
[Cloud Code][Validation] Pass in request object
2016-02-18 10:39:30 -08:00
Francis Lessard 5de33ac226 Fix : save whit masterKey cause object not found. 2016-02-18 13:29:26 -05:00
Fosco Marotto d087ae9b35 Merge pull request #471 from ParsePlatform/readme
Use new Parse Server logo.
2016-02-18 10:28:53 -08:00
Drew 19d17d8610 Merge pull request #478 from flovilmart/flovilmart.hotfix.facebook.appId.Validation
Moved the proper facebook auth data validation
2016-02-18 08:30:36 -08:00
Florent Vilmart d42b08055b Moved the proper facebook auth data validation 2016-02-18 11:23:02 -05:00
Drew b1ab388c96 Merge pull request #480 from flovilmart/flovilmart.fix.missingSessionToken
Fix missing session token when fetching a _User
2016-02-18 08:20:44 -08:00
Florent Vilmart cc55bfb7ba Fix missing session token when fetching a _User 2016-02-18 10:59:36 -05:00
Francis Lessard 67c7670437 Merge remote-tracking branch 'ParsePlatform/master' into user-roles 2016-02-18 10:06:12 -05:00
jim1_lin f271ac9ac1 ExportAdapter destroy nothing will cause change password failed
Changing password without any session will failed,
due to clear no session with response ObjectNotFound.
2016-02-18 15:36:21 +08:00
Drew Gross 61b4468dac Implement DELETE /schemas/:className 2016-02-17 19:00:17 -08:00
Héctor Ramos d668bd984c Remove padding around logo 2016-02-17 17:25:24 -08:00
Héctor Ramos 7cc4ef95c0 Use new Parse Server logo. 2016-02-17 15:07:00 -08:00
Fosco Marotto 57e94dc82c Merge pull request #470 from ParsePlatform/readme
New introduction and links to the documentation
2016-02-17 14:08:19 -08:00
Héctor Ramos a254759da8 New introduction and links to the documentation 2016-02-17 14:00:35 -08:00
Fosco Marotto b380cd2bd6 Merge pull request #467 from ParsePlatform/hramos-contributing
Fix broken link to CoC
2016-02-17 13:04:24 -08:00
Héctor Ramos 86a5ae40e3 Fix broken link to CoC 2016-02-17 13:02:14 -08:00
Björn Kaiser ce1de0a5ef Cloud Function validation now uses the complete request instead of just the request parameters 2016-02-17 20:43:09 +00:00
Björn Kaiser bd0d21af29 Merge remote-tracking branch 'upstream/master' 2016-02-17 20:35:17 +00:00
Simon Bengtsson 4ecf0ac10c Don't use https://api.parse.com/1 as default serverURL 2016-02-17 20:30:43 +01:00
Francis Lessard 176f1b9085 Merge remote-tracking branch 'ParsePlatform/master' into user-roles 2016-02-16 16:34:55 -05:00
Marco129 06b8157eea Throw error when query with wrongly encoded parameter 2016-02-17 02:52:32 +08:00
Francis Lessard 5a167a9a46 Merge remote-tracking branch 'ParsePlatform/master' into user-roles 2016-02-16 10:46:05 -05:00
Francis Lessard 5b40a589c0 Remove public ACL set by default. 2016-02-16 10:45:43 -05:00
Francis Lessard 2c5b77f284 Merge remote-tracking branch 'ParsePlatform/master' into user-roles 2016-02-16 09:58:38 -05:00
Francis Lessard 173c5d46e3 Merge remote-tracking branch 'ParsePlatform/master' into user-roles 2016-02-13 18:42:30 -05:00
Francis Lessard 2234a104d4 Merge remote-tracking branch 'ParsePlatform/master' into user-roles 2016-02-13 08:19:24 -05:00
Francis Lessard 83c0f92553 no message 2016-02-13 08:18:43 -05:00
vitaliysobur 0755a2953e fix Access-Control-Allow-Headers to match exact 2016-02-12 23:06:56 +00:00
Francis Lessard 220a13392d no message 2016-02-12 16:32:45 -05:00
Francis Lessard d7d87a31f0 Merge remote-tracking branch 'ParsePlatform/master' into user-roles 2016-02-12 16:16:20 -05:00
Björn Kaiser ec1bbc8d74 Merge remote-tracking branch 'upstream/master' 2016-02-12 20:37:10 +00:00
Francis Lessard 42aacdf62b FIX : User Roles not added to create, update or delete calls 2016-02-11 22:16:07 -05:00
Björn Kaiser 2ccd039497 Merge remote-tracking branch 'upstream/master' 2016-02-06 22:57:52 +00:00
Björn Kaiser 5420c38adf Merge pull request #3 from codegefluester/test_for_calling_nonexisting_cf
Test for calling non-existing Cloud Function
2016-02-03 22:12:58 +00:00
Björn Kaiser 4822d2b668 Merge branch 'master' into test_for_calling_nonexisting_cf 2016-02-03 22:08:30 +00:00
Björn Kaiser 6adffb09d0 Merge pull request #2 from codegefluester/cloud_code_validation
Added Cloud Function validation
2016-02-03 22:01:29 +00:00
Björn Kaiser 035fcd2615 Added test for proper handling of trying to call a non-existing Cloud Function 2016-02-03 21:21:30 +00:00
178 changed files with 18436 additions and 5027 deletions
+7 -3
View File
@@ -1,5 +1,9 @@
{
"presets": [
"es2015"
]
"plugins": [
"transform-flow-strip-types"
],
"presets": [
"es2015",
"stage-0"
]
}
+9
View File
@@ -0,0 +1,9 @@
[ignore]
.*/node_modules/
.*/lib/
[include]
[libs]
[options]
+19
View File
@@ -0,0 +1,19 @@
Check out [this issue](https://github.com/ParsePlatform/parse-server/issues/1271) for an ideal bug report. The closer your issue report is to that one, the more likely we are to be able to help, and the more likely we will be to fix the issue quickly!
For implementation related questions or technical support, please refer to the [Stack Overflow](http://stackoverflow.com/questions/tagged/parse.com) and [Server Fault](https://serverfault.com/tags/parse) communities.
Make sure these boxes are checked before submitting your issue -- thanks for reporting issues back to Parse Server!
- [ ] You've met the [prerequisites](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites).
- [ ] You're running the [latest version](https://github.com/ParsePlatform/parse-server/releases) of Parse Server.
- [ ] You've searched through [existing issues](https://github.com/ParsePlatform/parse-server/issues?utf8=%E2%9C%93&q=). Chances are that your issue has been reported or resolved before.
#### Environment Setup
#### Steps to reproduce
#### Logs/Trace
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+13
View File
@@ -1,5 +1,6 @@
# Logs
logs
test_logs
*.log
# Runtime data
@@ -32,5 +33,17 @@ node_modules
# WebStorm/IntelliJ
.idea
# visual studio code
.vscode
# Babel.js
lib/
# cache folder
.cache
# Mac DS_Store files
.DS_Store
# Folder created by FileSystemAdapter
/files
-33
View File
@@ -1,33 +0,0 @@
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
# Emacs
*~
# WebStorm/IntelliJ
.idea
+15 -6
View File
@@ -1,13 +1,22 @@
branches:
only:
- master
language: node_js
node_js:
- "4.3"
- '4.3'
env:
global:
- COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**'
matrix:
- MONGODB_VERSION=2.6.11
- MONGODB_VERSION=3.0.8
cache:
directories:
- $HOME/.mongodb/versions/downloads
after_success: ./node_modules/.bin/codecov
- "$HOME/.mongodb/versions/downloads"
after_success: "./node_modules/.bin/codecov"
deploy:
provider: npm
email:
secure: T3LwSJFcu632SDfCoavcKL39NktWLEOOFzonAjeHAglmMyDp9hvU8xHwQ4JJy2CRA03c+ezAe2gH3Va+jfxvh1JtFUan+p88vaCHHKuNwPSp4WZBPt1ZTDmG+ACG6j+ZWHK+eP7hLvtlCi/M9/v4/bVojpm7v01LLiM+MRhnE9E7eLemQC4zb6RVtf2oGQ3mX51kMeya218kEm5bsQPpRJElI9jwletFa8qYK5AFgaCHpll059RRHQTTE9MTNcN58P/Kf6Qb3KGpKNoaHTXoOD4U6LcXLWvdHONVB6AzGefxg2b/dvtaO4nd5lDooMBR3u9kWpopXkyAuf+fO/8je9JTxO6CDUtDjHRcR2mCEXWX6rculIAbNXOv1ccRYCTQ8aFtYnFlMSb1+QIAdyT6HHmeT5ktk3+XJRuEv0PJJvqHTo+j7tPngjiv5sPNutgGFlOFO90omTbzEkIT/D/LsgXbWm2QKYWejbLiuSINg+FiFiZN90doCf4aCpm2y1Q/9GzvK+eXcpqzPCGxOykE7EuPZK3+S/ZU2oaWL2uSpbKPtC0qxebrneR307aVEa7C85HCkTMMBzE4tjUr8h5HhLqtWvqmDcnBo3PfQhf9cEO6jQLd3qhEVJmcwKz/yOCfgPXlCbRqiKk7j94perpNCIalXtMI9ySYjJNjHsB4600=
api_key:
secure: WLjhHVAdUkDy6UdNWveTpZqphw9olN0alCpyPpU0cJjlN/hk7YxPP+YHOSVnMZaRZoHM0LL11jPKLf970nymBfvJHDWxKk71c/5xyTX8pBtSxmGmGy23a5g2VrHVMd2JxsI7NEAH500tlFz+01t4E3Steo0NpAkHR3Q51kj01gQy8IumsksfhRc9uvjZ7zjm99Yk4L+cxyei79v4esfpx7Bgm0HTfAAAG/19F+r0hvvFiV517SREDU/YtcX/rIjVepEy1uiLKEohnmYtVIRSA3Hjh6VlHVhdj2WDd/dYrxi/Ioysq2zOM9pZVLamO9asB1e6JrTEipWE9jSZXxsbnfFzuNLxcOjKv1wd3OzQKF/7pGKGiWRTv5Xm19D/FrXoE/ULs6bHcC/Ke8Gs2RxPlOCdvAFehCkyf5P4HOPCQdR7o8Yiuvt+5JWKBflElNbSd4nWgwUOo3Yv8vC4Vj53fwmM+Uqfu3IgYFWktYxCm4RWIKMfB/gtmjcF6QYFfjvEMwAvRfBV81kuynCsnubXWzQeBE/b3JOhBfpGciKCjKfy+tS6bZfFjCtQV98hMMiCPre8Y7PahHDYc65wU9Ake+ZE+dDaSbeV3DZ5JeifLJHzKW2J2dWeRANkOaiSwO9VBC9/rht5ulK5qQ1pB2+sKDToAaiKy6RMlB/HgIoYqsU=
on:
tags: true
repo: ParsePlatform/parse-server
+212 -2
View File
@@ -1,5 +1,215 @@
## Parse Server Changelog
### 2.2.6 (4/5/2016)
* Important Fix: Disables find on installation from clients [\#1374](https://github.com/ParsePlatform/parse-server/pull/1374) ([flovilmart](https://github.com/flovilmart))
* Adds missing options to the CLI [\#1368](https://github.com/ParsePlatform/parse-server/pull/1368) ([flovilmart](https://github.com/flovilmart))
* Removes only master on travis [\#1367](https://github.com/ParsePlatform/parse-server/pull/1367) ([flovilmart](https://github.com/flovilmart))
* Auth.\_loadRoles should not query the same role twice. [\#1366](https://github.com/ParsePlatform/parse-server/pull/1366) ([blacha](https://github.com/blacha))
### 2.2.5 (4/4/2016)
* Improves config loading and tests [\#1363](https://github.com/ParsePlatform/parse-server/pull/1363) ([flovilmart](https://github.com/flovilmart))
* Adds travis configuration to deploy NPM on new version tags [\#1361](https://github.com/ParsePlatform/parse-server/pull/1361) ([gfosco](https://github.com/gfosco))
* Inject the default schemas properties when loading it [\#1357](https://github.com/ParsePlatform/parse-server/pull/1357) ([flovilmart](https://github.com/flovilmart))
* Adds console transport when testing with VERBOSE=1 [\#1351](https://github.com/ParsePlatform/parse-server/pull/1351) ([flovilmart](https://github.com/flovilmart))
* Make notEqual work on relations [\#1350](https://github.com/ParsePlatform/parse-server/pull/1350) ([flovilmart](https://github.com/flovilmart))
* Accept only bool for $exists in LiveQuery [\#1315](https://github.com/ParsePlatform/parse-server/pull/1315) ([drew-gross](https://github.com/drew-gross))
* Adds more options when using CLI/config [\#1305](https://github.com/ParsePlatform/parse-server/pull/1305) ([flovilmart](https://github.com/flovilmart))
* Update error message [\#1297](https://github.com/ParsePlatform/parse-server/pull/1297) ([drew-gross](https://github.com/drew-gross))
* Properly let masterKey add fields [\#1291](https://github.com/ParsePlatform/parse-server/pull/1291) ([flovilmart](https://github.com/flovilmart))
* Point to \#1271 as how to write a good issue report [\#1290](https://github.com/ParsePlatform/parse-server/pull/1290) ([drew-gross](https://github.com/drew-gross))
* Adds ability to override mount with publicServerURL for production uses [\#1287](https://github.com/ParsePlatform/parse-server/pull/1287) ([flovilmart](https://github.com/flovilmart))
* Single object queries to use include and keys [\#1280](https://github.com/ParsePlatform/parse-server/pull/1280) ([jeremyjackson89](https://github.com/jeremyjackson89))
* Improves report for Push error in logs and \_PushStatus [\#1269](https://github.com/ParsePlatform/parse-server/pull/1269) ([flovilmart](https://github.com/flovilmart))
* Removes all stdout/err logs while testing [\#1268](https://github.com/ParsePlatform/parse-server/pull/1268) ([flovilmart](https://github.com/flovilmart))
* Matching queries with doesNotExist constraint [\#1250](https://github.com/ParsePlatform/parse-server/pull/1250) ([andrecardoso](https://github.com/andrecardoso))
* Added session length option for session tokens to server configuration [\#997](https://github.com/ParsePlatform/parse-server/pull/997) ([Kenishi](https://github.com/Kenishi))
* Regression test for \#1259 [\#1286](https://github.com/ParsePlatform/parse-server/pull/1286) ([drew-gross](https://github.com/drew-gross))
* Regression test for \#871 [\#1283](https://github.com/ParsePlatform/parse-server/pull/1283) ([drew-gross](https://github.com/drew-gross))
* Add a test to repro \#701 [\#1281](https://github.com/ParsePlatform/parse-server/pull/1281) ([drew-gross](https://github.com/drew-gross))
* Fix for \#1334: using relative cloud code files broken [\#1353](https://github.com/ParsePlatform/parse-server/pull/1353) ([airdrummingfool](https://github.com/airdrummingfool))
* Fix Issue/1288 [\#1346](https://github.com/ParsePlatform/parse-server/pull/1346) ([flovilmart](https://github.com/flovilmart))
* Fixes \#1271 [\#1295](https://github.com/ParsePlatform/parse-server/pull/1295) ([drew-gross](https://github.com/drew-gross))
* Fixes issue \#1302 [\#1314](https://github.com/ParsePlatform/parse-server/pull/1314) ([flovilmart](https://github.com/flovilmart))
* Fixes bug related to include in queries [\#1312](https://github.com/ParsePlatform/parse-server/pull/1312) ([flovilmart](https://github.com/flovilmart))
### 2.2.4 (3/29/2016)
* Hotfix: fixed imports issue for S3Adapter, GCSAdapter, FileSystemAdapter [\#1263](https://github.com/ParsePlatform/parse-server/pull/1263) ([drew-gross](https://github.com/drew-gross)
* Fix: Clean null authData values on _User update [\#1199](https://github.com/ParsePlatform/parse-server/pull/1199) ([yuzeh](https://github.com/yuzeh))
### 2.2.3 (3/29/2016)
* Fixed bug with invalid email verification link on email update. [\#1253](https://github.com/ParsePlatform/parse-server/pull/1253) ([kzielonka](https://github.com/kzielonka))
* Badge update supports increment as well as Increment [\#1248](https://github.com/ParsePlatform/parse-server/pull/1248) ([flovilmart](https://github.com/flovilmart))
* Config/Push Tested with the dashboard. [\#1235](https://github.com/ParsePlatform/parse-server/pull/1235) ([drew-gross](https://github.com/drew-gross))
* Better logging with winston [\#1234](https://github.com/ParsePlatform/parse-server/pull/1234) ([flovilmart](https://github.com/flovilmart))
* Make GlobalConfig work like parse.com [\#1210](https://github.com/ParsePlatform/parse-server/pull/1210) ([framp](https://github.com/framp))
* Improve flattening of results from pushAdapter [\#1204](https://github.com/ParsePlatform/parse-server/pull/1204) ([flovilmart](https://github.com/flovilmart))
* Push adapters are provided by external packages [\#1195](https://github.com/ParsePlatform/parse-server/pull/1195) ([flovilmart](https://github.com/flovilmart))
* Fix flaky test [\#1188](https://github.com/ParsePlatform/parse-server/pull/1188) ([drew-gross](https://github.com/drew-gross))
* Fixes problem affecting finding array pointers [\#1185](https://github.com/ParsePlatform/parse-server/pull/1185) ([flovilmart](https://github.com/flovilmart))
* Moves Files adapters to external packages [\#1172](https://github.com/ParsePlatform/parse-server/pull/1172) ([flovilmart](https://github.com/flovilmart))
* Mark push as enabled in serverInfo endpoint [\#1164](https://github.com/ParsePlatform/parse-server/pull/1164) ([drew-gross](https://github.com/drew-gross))
* Document email adapter [\#1144](https://github.com/ParsePlatform/parse-server/pull/1144) ([drew-gross](https://github.com/drew-gross))
* Reset password fix [\#1133](https://github.com/ParsePlatform/parse-server/pull/1133) ([carmenlau](https://github.com/carmenlau))
### 2.2.2 (3/23/2016)
* Important Fix: Mounts createLiveQueryServer, fix babel induced problem [\#1153](https://github.com/ParsePlatform/parse-server/pull/1153) (flovilmart)
* Move ParseServer to it's own file [\#1166](https://github.com/ParsePlatform/parse-server/pull/1166) (flovilmart)
* Update README.md - remove deploy buttons - replace with community links [\#1139](https://github.com/ParsePlatform/parse-server/pull/1139) (drew-gross)
* Adds bootstrap.sh [\#1138](https://github.com/ParsePlatform/parse-server/pull/1138) (flovilmart)
* Fix: Do not override username [\#1142](https://github.com/ParsePlatform/parse-server/pull/1142) (flovilmart)
* Fix: Add pushId back to GCM payload [\#1168](https://github.com/ParsePlatform/parse-server/pull/1168) (wangmengyan95)
### 2.2.1 (3/22/2016)
* New: Add FileSystemAdapter file adapter [\#1098](https://github.com/ParsePlatform/parse-server/pull/1098) (dtsolis)
* New: Enabled CLP editing [\#1128](https://github.com/ParsePlatform/parse-server/pull/1128) (drew-gross)
* Improvement: Reduces the number of connections to mongo created [\#1111](https://github.com/ParsePlatform/parse-server/pull/1111) (flovilmart)
* Improvement: Make ParseServer a class [\#980](https://github.com/ParsePlatform/parse-server/pull/980) (flovilmart)
* Fix: Adds support for plain object in $add, $addUnique, $remove [\#1114](https://github.com/ParsePlatform/parse-server/pull/1114) (flovilmart)
* Fix: Generates default CLP, freezes objects [\#1132](https://github.com/ParsePlatform/parse-server/pull/1132) (flovilmart)
* Fix: Properly sets installationId on creating session with 3rd party auth [\#1110](https://github.com/ParsePlatform/parse-server/pull/1110) (flovilmart)
### 2.2.0 (3/18/2016)
* New Feature: Real-time functionality with Live Queries! [\#1092](https://github.com/ParsePlatform/parse-server/pull/1092) (wangmengyan95)
* Improvement: Push Status API [\#1004](https://github.com/ParsePlatform/parse-server/pull/1004) (flovilmart)
* Improvement: Allow client operations on Roles [\#1068](https://github.com/ParsePlatform/parse-server/pull/1068) (flovilmart)
* Improvement: Add URI encoding to mongo auth parameters [\#986](https://github.com/ParsePlatform/parse-server/pull/986) (bgw)
* Improvement: Adds support for apps key in config file, but only support single app for now [\#979](https://github.com/ParsePlatform/parse-server/pull/979) (flovilmart)
* Documentation: Getting Started and Configuring Parse Server [\#988](https://github.com/ParsePlatform/parse-server/pull/988) (hramos)
* Fix: Various edge cases with REST API [\#1066](https://github.com/ParsePlatform/parse-server/pull/1066) (flovilmart)
* Fix: Makes sure the location in results has the proper objectId [\#1065](https://github.com/ParsePlatform/parse-server/pull/1065) (flovilmart)
* Fix: Third-party auth is properly removed when unlinked [\#1081](https://github.com/ParsePlatform/parse-server/pull/1081) (flovilmart)
* Fix: Clear the session-user cache when changing \_User objects [\#1072](https://github.com/ParsePlatform/parse-server/pull/1072) (gfosco)
* Fix: Bug related to subqueries on unfetched objects [\#1046](https://github.com/ParsePlatform/parse-server/pull/1046) (flovilmart)
* Fix: Properly urlencode parameters for email validation and password reset [\#1001](https://github.com/ParsePlatform/parse-server/pull/1001) (flovilmart)
* Fix: Better sanitization/decoding of object data for afterSave triggers [\#992](https://github.com/ParsePlatform/parse-server/pull/992) (flovilmart)
* Fix: Changes default encoding for httpRequest [\#892](https://github.com/ParsePlatform/parse-server/pull/892) (flovilmart)
### 2.1.6 (3/11/2016)
* Improvement: Full query support for badge Increment \(\#931\) [\#983](https://github.com/ParsePlatform/parse-server/pull/983) (flovilmart)
* Improvement: Shutdown standalone parse server gracefully [\#958](https://github.com/ParsePlatform/parse-server/pull/958) (raulr)
* Improvement: Add database options to ParseServer constructor and pass to MongoStorageAdapter [\#956](https://github.com/ParsePlatform/parse-server/pull/956) (steven-supersolid)
* Improvement: AuthData logic refactor [\#952](https://github.com/ParsePlatform/parse-server/pull/952) (flovilmart)
* Improvement: Changed FileLoggerAdapterSpec to fail gracefully on Windows [\#946](https://github.com/ParsePlatform/parse-server/pull/946) (aneeshd16)
* Improvement: Add new schema collection type and replace all usages of direct mongo collection for schema operations. [\#943](https://github.com/ParsePlatform/parse-server/pull/943) (nlutsenko)
* Improvement: Adds CLP API to Schema router [\#898](https://github.com/ParsePlatform/parse-server/pull/898) (flovilmart)
* Fix: Cleans up authData null keys on login for android crash [\#978](https://github.com/ParsePlatform/parse-server/pull/978) (flovilmart)
* Fix: Do master query for before/afterSaveHook [\#959](https://github.com/ParsePlatform/parse-server/pull/959) (wangmengyan95)
* Fix: re-add shebang [\#944](https://github.com/ParsePlatform/parse-server/pull/944) (flovilmart)
* Fix: Added test command for Windows support [\#886](https://github.com/ParsePlatform/parse-server/pull/886) (aneeshd16)
### 2.1.5 (3/9/2016)
* New: FileAdapter for Google Cloud Storage [\#708](https://github.com/ParsePlatform/parse-server/pull/708) (mcdonamp)
* Improvement: Minimize extra schema queries in some scenarios. [\#919](https://github.com/ParsePlatform/parse-server/pull/919) (Marco129)
* Improvement: Move DatabaseController and Schema fully to adaptive mongo collection. [\#909](https://github.com/ParsePlatform/parse-server/pull/909) (nlutsenko)
* Improvement: Cleanup PushController/PushRouter, remove raw mongo collection access. [\#903](https://github.com/ParsePlatform/parse-server/pull/903) (nlutsenko)
* Improvement: Increment badge the right way [\#902](https://github.com/ParsePlatform/parse-server/pull/902) (flovilmart)
* Improvement: Migrate ParseGlobalConfig to new database storage API. [\#901](https://github.com/ParsePlatform/parse-server/pull/901) (nlutsenko)
* Improvement: Improve delete flow for non-existent \_Join collection [\#881](https://github.com/ParsePlatform/parse-server/pull/881) (Marco129)
* Improvement: Adding a role scenario test for issue 827 [\#878](https://github.com/ParsePlatform/parse-server/pull/878) (gfosco)
* Improvement: Test empty authData block on login for \#413 [\#863](https://github.com/ParsePlatform/parse-server/pull/863) (gfosco)
* Improvement: Modified the npm dev script to support Windows [\#846](https://github.com/ParsePlatform/parse-server/pull/846) (aneeshd16)
* Improvement: Move HooksController to use MongoCollection instead of direct Mongo access. [\#844](https://github.com/ParsePlatform/parse-server/pull/844) (nlutsenko)
* Improvement: Adds public\_html and views for packaging [\#839](https://github.com/ParsePlatform/parse-server/pull/839) (flovilmart)
* Improvement: Better support for windows builds [\#831](https://github.com/ParsePlatform/parse-server/pull/831) (flovilmart)
* Improvement: Convert Schema.js to ES6 class. [\#826](https://github.com/ParsePlatform/parse-server/pull/826) (nlutsenko)
* Improvement: Remove duplicated instructions [\#816](https://github.com/ParsePlatform/parse-server/pull/816) (hramos)
* Improvement: Completely migrate SchemasRouter to new MongoCollection API. [\#794](https://github.com/ParsePlatform/parse-server/pull/794) (nlutsenko)
* Fix: Do not require where clause in $dontSelect condition on queries. [\#925](https://github.com/ParsePlatform/parse-server/pull/925) (nlutsenko)
* Fix: Make sure that ACLs propagate to before/after save hooks. [\#924](https://github.com/ParsePlatform/parse-server/pull/924) (nlutsenko)
* Fix: Support params option in Parse.Cloud.httpRequest. [\#912](https://github.com/ParsePlatform/parse-server/pull/912) (carmenlau)
* Fix: Fix flaky Parse.GeoPoint test. [\#908](https://github.com/ParsePlatform/parse-server/pull/908) (nlutsenko)
* Fix: Handle legacy \_client\_permissions key in \_SCHEMA. [\#900](https://github.com/ParsePlatform/parse-server/pull/900) (drew-gross)
* Fix: Fixes bug when querying equalTo on objectId and relation [\#887](https://github.com/ParsePlatform/parse-server/pull/887) (flovilmart)
* Fix: Allow crossdomain on filesRouter [\#876](https://github.com/ParsePlatform/parse-server/pull/876) (flovilmart)
* Fix: Remove limit when counting results. [\#867](https://github.com/ParsePlatform/parse-server/pull/867) (gfosco)
* Fix: beforeSave changes should propagate to the response [\#865](https://github.com/ParsePlatform/parse-server/pull/865) (gfosco)
* Fix: Delete relation field when \_Join collection not exist [\#864](https://github.com/ParsePlatform/parse-server/pull/864) (Marco129)
* Fix: Related query on non-existing column [\#861](https://github.com/ParsePlatform/parse-server/pull/861) (gfosco)
* Fix: Update markdown in .github/ISSUE\_TEMPLATE.md [\#859](https://github.com/ParsePlatform/parse-server/pull/859) (igorshubovych)
* Fix: Issue with creating wrong \_Session for Facebook login [\#857](https://github.com/ParsePlatform/parse-server/pull/857) (tobernguyen)
* Fix: Leak warnings in tests, use mongodb-runner from node\_modules [\#843](https://github.com/ParsePlatform/parse-server/pull/843) (drew-gross)
* Fix: Reversed roles lookup [\#841](https://github.com/ParsePlatform/parse-server/pull/841) (flovilmart)
* Fix: Improves loading of Push Adapter, fix loading of S3Adapter [\#833](https://github.com/ParsePlatform/parse-server/pull/833) (flovilmart)
* Fix: Add field to system schema [\#828](https://github.com/ParsePlatform/parse-server/pull/828) (Marco129)
### 2.1.4 (3/3/2016)
* New: serverInfo endpoint that returns server version and info about the server's features
* Improvement: Add support for badges on iOS
* Improvement: Improve failure handling in cloud code http requests
* Improvement: Add support for queries on pointers and relations
* Improvement: Add support for multiple $in clauses in a query
* Improvement: Add allowClientClassCreation config option
* Improvement: Allow atomically setting subdocument keys
* Improvement: Allow arbitrarily deeply nested roles
* Improvement: Set proper content-type in S3 File Adapter
* Improvement: S3 adapter auto-creates buckets
* Improvement: Better error messages for many errors
* Performance: Improved algorithm for validating client keys
* Experimental: Parse Hooks and Hooks API
* Experimental: Email verification and password reset emails
* Experimental: Improve compatability of logs feature with Parse.com
* Fix: Fix for attempting to delete missing classes via schemas API
* Fix: Allow creation of system classes via schemas API
* Fix: Allow missing where cause in $select
* Fix: Improve handling of invalid object ids
* Fix: Replace query overwriting existing query
* Fix: Propagate installationId in cloud code triggers
* Fix: Session expiresAt is now a Date instead of a string
* Fix: Fix count queries
* Fix: Disallow _Role objects without names or without ACL
* Fix: Better handling of invalid types submitted
* Fix: beforeSave will not be triggered for attempts to save with invalid authData
* Fix: Fix duplicate device token issues on Android
* Fix: Allow empty authData on signup
* Fix: Allow Master Key Headers (CORS)
* Fix: Fix bugs if JavaScript key was not provided in server configuration
* Fix: Parse Files on objects can now be stored without URLs
* Fix: allow both objectId or installationId when modifying installation
* Fix: Command line works better when not given options
### 2.1.3 (2/24/2016)
* Feature: Add initial support for in-app purchases
* Feature: Better error messages when attempting to run the server on a port that is already in use or without a server URL
* Feature: Allow customization of max file size
* Performance: Faster saves if not using beforeSave triggers
* Fix: Send session token in response to current user endpoint
* Fix: Remove triggers for _Session collection
* Fix: Improve compatability of cloud code beforeSave hook for newly created object
* Fix: ACL creation for master key only objects
* Fix: Allow uploading files without Content-Type
* Fix: Add features to http requrest to match Parse.com
* Fix: Bugs in development script when running from locations other than project root
* Fix: Can pass query constraints in URL
* Fix: Objects with legacy "_tombstone" key now don't cause issues.
* Fix: Allow nested keys in objects to begin with underscores
* Fix: Allow correct headers for CORS
### 2.1.2 (2/19/2016)
* Change: The S3 file adapter constructor requires a bucket name
* Fix: Parse Query should throw if improperly encoded
* Fix: Issue where roles were not used in some requests
* Fix: serverURL will no longer default to api.parse.com/1
### 2.1.1 (2/18/2016)
* Experimental: Schemas API support for DELETE operations
* Fix: Session token issue fetching Users
* Fix: Facebook auth validation
* Fix: Invalid error when deleting missing session
### 2.1.0 (2/17/2016)
* Feature: Support for additional OAuth providers
@@ -9,7 +219,7 @@
* Feature: Support for logs, extensible via Log Adapter
* Feature: New Push Adapter for sending push notifications through OneSignal
* Feature: Tighter default security for Users
* Feature: Pass parameters to Cloud Code in query string
* Feature: Pass parameters to cloud code in query string
* Feature: Disable anonymous users via configuration.
* Experimental: Schemas API support for PUT operations
* Fix: Prevent installation ID from being added to User
@@ -26,7 +236,7 @@
### 2.0.8 (2/11/2016)
* Add: support for Android and iOS push notifications
* Experimental: Cloud Code validation hooks (can mark as non-experimental after we have docs)
* Experimental: cloud code validation hooks (can mark as non-experimental after we have docs)
* Experimental: support for schemas API (GET and POST only)
* Experimental: support for Parse Config (GET and POST only)
* Fix: Querying objects with equality constraint on array column
+1 -4
View File
@@ -12,7 +12,4 @@ We really want Parse to be yours, to see it grow and thrive in the open source c
##### Code of Conduct
This project adheres to the [Open Code of Conduct][code-of-conduct]. By participating, you are expected to honor this code.
[code-of-conduct]: http://todogroup.org/opencodeofconduct/#Parse Server/fjm@fb.com
This project adheres to the [Open Code of Conduct](http://todogroup.org/opencodeofconduct/#Parse Server/fjm@fb.com). By participating, you are expected to honor this code.
+201 -129
View File
@@ -1,194 +1,266 @@
## parse-server
![Parse Server logo](.github/parse-server-logo.png?raw=true)
[![Build Status](https://img.shields.io/travis/ParsePlatform/parse-server/master.svg?style=flat)](https://travis-ci.org/ParsePlatform/parse-server)
[![Coverage Status](https://img.shields.io/codecov/c/github/ParsePlatform/parse-server/master.svg)](https://codecov.io/github/ParsePlatform/parse-server?branch=master)
[![npm version](https://img.shields.io/npm/v/parse-server.svg?style=flat)](https://www.npmjs.com/package/parse-server)
A Parse.com API compatible router package for Express
Parse Server is an [open source version of the Parse backend](http://blog.parse.com/announcements/introducing-parse-server-and-the-database-migration-tool/) that can be deployed to any infrastructure that can run Node.js.
Read the announcement blog post here: http://blog.parse.com/announcements/introducing-parse-server-and-the-database-migration-tool/
Parse Server works with the Express web application framework. It can be added to existing web applications, or run by itself.
Read the migration guide here: https://parse.com/docs/server/guide#migrating
# Getting Started
There is a development wiki here on GitHub: https://github.com/ParsePlatform/parse-server/wiki
The fastest and easiest way to get started is to run MongoDB and Parse Server locally.
We also have an [example project](https://github.com/ParsePlatform/parse-server-example) using the parse-server module on Express.
---
#### Basic options:
* databaseURI (required) - The connection string for your database, i.e. `mongodb://user:pass@host.com/dbname`
* appId (required) - The application id to host with this server instance
* masterKey (required) - The master key to use for overriding ACL security
* cloud - The absolute path to your cloud code main.js file
* fileKey - For migrated apps, this is necessary to provide access to files already hosted on Parse.
* facebookAppIds - An array of valid Facebook application IDs.
* serverURL - URL which will be used by Cloud Code functions to make requests against.
* push - Configuration options for APNS and GCM push. See the [wiki entry](https://github.com/ParsePlatform/parse-server/wiki/Push).
#### Client key options:
The client keys used with Parse are no longer necessary with parse-server. If you wish to still require them, perhaps to be able to refuse access to older clients, you can set the keys at initialization time. Setting any of these keys will require all requests to provide one of the configured keys.
* clientKey
* javascriptKey
* restAPIKey
* dotNetKey
#### OAuth Support
parse-server supports 3rd party authentication with
* Twitter
* Meetup
* Linkedin
* Google
* Instagram
* Facebook
Configuration options for these 3rd-party modules is done with the oauth option passed to ParseServer:
## Running Parse Server locally
```
{
oauth: {
twitter: {
consumer_key: "", // REQUIRED
consumer_secret: "" // REQUIRED
},
facebook: {
appIds: "FACEBOOK APP ID"
}
}
$ npm install -g parse-server mongodb-runner
$ mongodb-runner start
$ parse-server --appId APPLICATION_ID --masterKey MASTER_KEY
```
You can use any arbitrary string as your application id and master key. These will be used by your clients to authenticate with the Parse Server.
That's it! You are now running a standalone version of Parse Server on your machine.
**Using a remote MongoDB?** Pass the `--databaseURI DATABASE_URI` parameter when starting `parse-server`. Learn more about configuring Parse Server [here](#configuration). For a full list of available options, run `parse-server --help`.
### Saving your first object
Now that you're running Parse Server, it is time to save your first object. We'll use the [REST API](https://parse.com/docs/rest/guide), but you can easily do the same using any of the [Parse SDKs](https://parseplatform.github.io/#sdks). Run the following:
```bash
curl -X POST \
-H "X-Parse-Application-Id: APPLICATION_ID" \
-H "Content-Type: application/json" \
-d '{"score":1337,"playerName":"Sean Plott","cheatMode":false}' \
http://localhost:1337/parse/classes/GameScore
```
You should get a response similar to this:
```js
{
"objectId": "2ntvSpRGIK",
"createdAt": "2016-03-11T23:51:48.050Z"
}
```
#### Custom Authentication
It is possible to leverage the OAuth support with any 3rd party authentication that you bring in.
You can now retrieve this object directly (make sure to replace `2ntvSpRGIK` with the actual `objectId` you received when the object was created):
```bash
$ curl -X GET \
-H "X-Parse-Application-Id: APPLICATION_ID" \
http://localhost:1337/parse/classes/GameScore/2ntvSpRGIK
```
```json
// Response
{
oauth: {
my_custom_auth: {
module: "PATH_TO_MODULE" // OR object,
option1: "",
option2: "",
}
}
"objectId": "2ntvSpRGIK",
"score": 1337,
"playerName": "Sean Plott",
"cheatMode": false,
"updatedAt": "2016-03-11T23:51:48.050Z",
"createdAt": "2016-03-11T23:51:48.050Z"
}
```
On this module, you need to implement and export those two functions `validateAuthData(authData, options) {} ` and `validateAppId(appIds, authData) {}`.
Keeping tracks of individual object ids is not ideal, however. In most cases you will want to run a query over the collection, like so:
For more informations about custom auth please see the examples:
```
$ curl -X GET \
-H "X-Parse-Application-Id: APPLICATION_ID" \
http://localhost:1337/parse/classes/GameScore
```
```json
// The response will provide all the matching objects within the `results` array:
{
"results": [
{
"objectId": "2ntvSpRGIK",
"score": 1337,
"playerName": "Sean Plott",
"cheatMode": false,
"updatedAt": "2016-03-11T23:51:48.050Z",
"createdAt": "2016-03-11T23:51:48.050Z"
}
]
}
- [facebook OAuth](https://github.com/ParsePlatform/parse-server/blob/master/src/oauth/facebook.js)
- [twitter OAuth](https://github.com/ParsePlatform/parse-server/blob/master/src/oauth/twitter.js)
- [instagram OAuth](https://github.com/ParsePlatform/parse-server/blob/master/src/oauth/instagram.js)
```
To learn more about using saving and querying objects on Parse Server, check out the [Parse documentation](https://parse.com/docs).
#### Advanced options:
### Connect your app to Parse Server
* filesAdapter - The default behavior (GridStore) can be changed by creating an adapter class (see [`FilesAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Files/FilesAdapter.js))
* databaseAdapter (unfinished) - The backing store can be changed by creating an adapter class (see `DatabaseAdapter.js`)
* loggerAdapter - The default behavior/transport (File) can be changed by creating an adapter class (see [`LoggerAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Logger/LoggerAdapter.js))
* enableAnonymousUsers - Defaults to true. Set to false to disable anonymous users.
---
Parse provides SDKs for all the major platforms. Refer to the Parse Server guide to [learn how to connect your app to Parse Server](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#using-parse-sdks-with-parse-server).
### Usage
## Running Parse Server elsewhere
You can create an instance of ParseServer, and mount it on a new or existing Express website:
Once you have a better understanding of how the project works, please refer to the [Parse Server wiki](https://github.com/ParsePlatform/parse-server/wiki) for in-depth guides to deploy Parse Server to major infrastructure providers. Read on to learn more about additional ways of running Parse Server.
### Parse Server Sample Application
We have provided a basic [Node.js application](https://github.com/ParsePlatform/parse-server-example) that uses the Parse Server module on Express and can be easily deployed to various infrastructure providers:
* [Heroku and mLab](https://devcenter.heroku.com/articles/deploying-a-parse-server-to-heroku)
* [AWS and Elastic Beanstalk](http://mobile.awsblog.com/post/TxCD57GZLM2JR/How-to-set-up-Parse-Server-on-AWS-using-AWS-Elastic-Beanstalk)
* [Digital Ocean](https://www.digitalocean.com/community/tutorials/how-to-run-parse-server-on-ubuntu-14-04)
* [NodeChef](https://nodechef.com/blog/post/6/migrate-from-parse-to-nodechef%E2%80%99s-managed-parse-server)
* [Google App Engine](https://medium.com/@justinbeckwith/deploying-parse-server-to-google-app-engine-6bc0b7451d50)
* [Microsoft Azure](https://azure.microsoft.com/en-us/blog/azure-welcomes-parse-developers/)
* [Pivotal Web Services](https://github.com/cf-platform-eng/pws-parse-server)
* [Back4app](http://blog.back4app.com/2016/03/01/quick-wizard-migration/)
### Parse Server + Express
You can also create an instance of Parse Server, and mount it on a new or existing Express website:
```js
var express = require('express');
var ParseServer = require('parse-server').ParseServer;
var app = express();
var port = process.env.PORT || 1337;
// Specify the connection string for your mongodb database
// and the location to your Parse cloud code
var api = new ParseServer({
databaseURI: 'mongodb://localhost:27017/dev',
cloud: '/home/myApp/cloud/main.js', // Provide an absolute path
databaseURI: 'mongodb://localhost:27017/dev', // Connection string for your MongoDB database
cloud: '/home/myApp/cloud/main.js', // Absolute path to your Cloud Code
appId: 'myAppId',
masterKey: '', //Add your master key here. Keep it secret!
masterKey: 'myMasterKey', // Keep this key secret!
fileKey: 'optionalFileKey',
serverURL: 'http://localhost:' + port + '/parse' // Don't forget to change to https if needed
serverURL: 'http://localhost:1337/parse' // Don't forget to change to https if needed
});
// Serve the Parse API on the /parse URL prefix
app.use('/parse', api);
// Hello world
app.get('/', function(req, res) {
res.status(200).send('Express is running here.');
app.listen(1337, function() {
console.log('parse-server-example running on port 1337.');
});
app.listen(port, function() {
console.log('parse-server-example running on port ' + port + '.');
});
```
For a full list of available options, run `parse-server --help`.
#### Standalone usage
# Documentation
You can configure the Parse Server with environment variables:
The full documentation for Parse Server is available in the [wiki](https://github.com/ParsePlatform/parse-server/wiki). The [Parse Server guide](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide) is a good place to get started. If you're interested in developing for Parse Server, the [Development guide](https://github.com/ParsePlatform/parse-server/wiki/Development-Guide) will help you get set up.
```js
## Migrating an Existing Parse App
The hosted version of Parse will be fully retired on January 28th, 2017. If you are planning to migrate an app, you need to begin work as soon as possible. There are a few areas where Parse Server does not provide compatibility with the hosted version of Parse. Learn more in the [Migration guide](https://github.com/ParsePlatform/parse-server/wiki/Migrating-an-Existing-Parse-App).
## Configuration
Parse Server can be configured using the following options. You may pass these as parameters when running a standalone `parse-server`, or by loading a configuration file in JSON format using `parse-server path/to/configuration.json`. If you're using Parse Server on Express, you may also pass these to the `ParseServer` object as options.
For the full list of available options, run `parse-server --help`.
#### Basic options
* `appId` **(required)** - The application id to host with this server instance. You can use any arbitrary string. For migrated apps, this should match your hosted Parse app.
* `masterKey` **(required)** - The master key to use for overriding ACL security. You can use any arbitrary string. Keep it secret! For migrated apps, this should match your hosted Parse app.
* `databaseURI` **(required)** - The connection string for your database, i.e. `mongodb://user:pass@host.com/dbname`.
* `port` - The default port is 1337, specify this parameter to use a different port.
* `serverURL` - URL to your Parse Server (don't forget to specify http:// or https://). This URL will be used when making requests to Parse Server from Cloud Code.
* `cloud` - The absolute path to your cloud code `main.js` file.
* `push` - Configuration options for APNS and GCM push. See the [Push Notifications wiki entry](https://github.com/ParsePlatform/parse-server/wiki/Push).
#### Client key options
The client keys used with Parse are no longer necessary with Parse Server. If you wish to still require them, perhaps to be able to refuse access to older clients, you can set the keys at initialization time. Setting any of these keys will require all requests to provide one of the configured keys.
* `clientKey`
* `javascriptKey`
* `restAPIKey`
* `dotNetKey`
#### Advanced options
* `fileKey` - For migrated apps, this is necessary to provide access to files already hosted on Parse.
* `allowClientClassCreation` - Set to false to disable client class creation. Defaults to true.
* `enableAnonymousUsers` - Set to false to disable anonymous users. Defaults to true.
* `oauth` - Used to configure support for [3rd party authentication](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#oauth).
* `facebookAppIds` - An array of valid Facebook application IDs that users may authenticate with.
* `mountPath` - Mount path for the server. Defaults to `/parse`.
* `filesAdapter` - The default behavior (GridStore) can be changed by creating an adapter class (see [`FilesAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Files/FilesAdapter.js)).
* `maxUploadSize` - Max file size for uploads. Defaults to 20 MB.
* `loggerAdapter` - The default behavior/transport (File) can be changed by creating an adapter class (see [`LoggerAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Logger/LoggerAdapter.js)).
* `sessionLength` - The length of time in seconds that a session should be valid for. Defaults to 31536000 seconds (1 year).
##### Email verification and password reset
Verifying user email addresses and enabling password reset via email requries an email adapter. As part of the `parse-server` package we provide an adapter for sending email through Mailgun. To use it, sign up for Mailgun, and add this to your initialization code:
```js
var server = ParseServer({
...otherOptions,
// Enable email verification
verifyUserEmails: true,
// The public URL of your app.
// This will appear in the link that is used to verify email addresses and reset passwords.
publicServerURL: 'https://example.com',
// Your apps name. This will appear in the subject and body of the emails that are sent.
appName: 'Parse App',
// The email adapter
emailAdapter: {
module: 'parse-server-simple-mailgun-adapter',
options: {
// The address that your emails come from
fromAddress: 'parse@example.com',
// Your domain from mailgun.com
domain: 'example.com',
// Your API key from mailgun.com
apiKey: 'key-mykey',
}
}
});
```
You can also use other email adapters contributed by the community such as [parse-server-sendgrid-adapter](https://www.npmjs.com/package/parse-server-sendgrid-adapter).
### Using environment variables to configure Parse Server
You may configure the Parse Server using environment variables:
```bash
PORT
PARSE_SERVER_APPLICATION_ID
PARSE_SERVER_MASTER_KEY
PARSE_SERVER_DATABASE_URI
PARSE_SERVER_URL
PARSE_SERVER_CLOUD_CODE_MAIN
PARSE_SERVER_COLLECTION_PREFIX
PARSE_SERVER_APPLICATION_ID // required
PARSE_SERVER_CLIENT_KEY
PARSE_SERVER_REST_API_KEY
PARSE_SERVER_DOTNET_KEY
PARSE_SERVER_JAVASCRIPT_KEY
PARSE_SERVER_DOTNET_KEY
PARSE_SERVER_MASTER_KEY // required
PARSE_SERVER_FILE_KEY
PARSE_SERVER_FACEBOOK_APP_IDS // string of comma separated list
```
The default port is 1337, to use a different port set the PORT environment variable:
```bash
$ PORT=8080 parse-server --appId APPLICATION_ID --masterKey MASTER_KEY
```
Alernatively, you can use the `PARSE_SERVER_OPTIONS` environment variable set to the JSON of your configuration (see Usage).
For the full list of configurable environment variables, run `parse-server --help`.
To start the server, just run `npm start`.
### Configuring File Adapters
##### Global installation
Parse Server allows developers to choose from several options when hosting files:
You can install parse-server globally
* `GridStoreAdapter`, which is backed by MongoDB;
* `S3Adapter`, which is backed by [Amazon S3](https://aws.amazon.com/s3/); or
* `GCSAdapter`, which is backed by [Google Cloud Storage](https://cloud.google.com/storage/)
`$ npm install -g parse-server`
`GridStoreAdapter` is used by default and requires no setup, but if you're interested in using S3 or Google Cloud Storage, additional configuration information is available in the [Parse Server wiki](https://github.com/ParsePlatform/parse-server/wiki/Configuring-File-Adapters).
Now you can just run `$ parse-server` from your command line.
# Support
For implementation related questions or technical support, please refer to the [Stack Overflow](http://stackoverflow.com/questions/tagged/parse.com) and [Server Fault](https://serverfault.com/tags/parse) communities.
### Supported
If you believe you've found an issue with Parse Server, make sure these boxes are checked before [reporting an issue](https://github.com/ParsePlatform/parse-server/issues):
* CRUD operations
* Schema validation
* Pointers
* Users, including Facebook login and anonymous users
* Files
* Push Notifications - See the [wiki entry](https://github.com/ParsePlatform/parse-server/wiki/Push).
* Installations
* Sessions
* Geopoints
* Roles
* Class-level Permissions (see below)
- [ ] You've met the [prerequisites](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites).
Parse server does not include a web-based dashboard, which is where class-level permissions have always been configured. If you migrate an app from Parse, you'll see the format for CLPs in the SCHEMA collection. There is also a `setPermissions` method on the `Schema` class, which you can see used in the unit-tests in `Schema.spec.js`
You can also set up an app on Parse, providing the connection string for your mongo database, and continue to use the dashboard on Parse.com.
- [ ] You're running the [latest version](https://github.com/ParsePlatform/parse-server/releases) of Parse Server.
### Not supported
- [ ] You've searched through [existing issues](https://github.com/ParsePlatform/parse-server/issues?utf8=%E2%9C%93&q=). Chances are that your issue has been reported or resolved before.
* `Parse.User.current()` or `Parse.Cloud.useMasterKey()` in cloud code. Instead of `Parse.User.current()` use `request.user` and instead of `Parse.Cloud.useMasterKey()` pass `useMasterKey: true` to each query. To make queries and writes as a specific user within Cloud Code, you need the user's session token, which is available in `request.user.getSessionToken()`.
# Contributing
We really want Parse to be yours, to see it grow and thrive in the open source community. Please see the [Contributing to Parse Server guide](CONTRIBUTING.md).
+6 -3
View File
@@ -4,14 +4,17 @@ var nodemon = require('nodemon');
var babel = require("babel-core");
var gaze = require('gaze');
var fs = require('fs');
var path = require('path');
// Watch the src and transpile when changed
gaze('src/**/*', function(err, watcher) {
if (err) throw err;
watcher.on('changed', function(file) {
console.log(file + " has changed");
watcher.on('changed', function(sourceFile) {
console.log(sourceFile + " has changed");
try {
fs.writeFile(file.replace(/\/src\//, "/lib/"), babel.transformFileSync(file).code);
targetFile = path.relative(__dirname, sourceFile).replace(/\/src\//, '/lib/');
targetFile = path.resolve(__dirname, targetFile);
fs.writeFile(targetFile, babel.transformFileSync(sourceFile).code);
} catch (e) {
console.error(e.message, e.stack);
}
+1 -45
View File
@@ -1,47 +1,3 @@
#!/usr/bin/env node
var express = require('express');
var ParseServer = require("../lib/index").ParseServer;
var app = express();
var options = {};
if (process.env.PARSE_SERVER_OPTIONS) {
options = JSON.parse(process.env.PARSE_SERVER_OPTIONS);
} else {
options.databaseURI = process.env.PARSE_SERVER_DATABASE_URI;
options.cloud = process.env.PARSE_SERVER_CLOUD_CODE_MAIN;
options.collectionPrefix = process.env.PARSE_SERVER_COLLECTION_PREFIX;
// Keys and App ID
options.appId = process.env.PARSE_SERVER_APPLICATION_ID;
options.clientKey = process.env.PARSE_SERVER_CLIENT_KEY;
options.restAPIKey = process.env.PARSE_SERVER_REST_API_KEY;
options.dotNetKey = process.env.PARSE_SERVER_DOTNET_KEY;
options.javascriptKey = process.env.PARSE_SERVER_JAVASCRIPT_KEY;
options.masterKey = process.env.PARSE_SERVER_MASTER_KEY;
options.fileKey = process.env.PARSE_SERVER_FILE_KEY;
// Comma separated list of facebook app ids
var facebookAppIds = process.env.PARSE_SERVER_FACEBOOK_APP_IDS;
if (facebookAppIds) {
facebookAppIds = facebookAppIds.split(",");
options.facebookAppIds = facebookAppIds;
}
var oauth = process.env.PARSE_SERVER_OAUTH_PROVIDERS;
if (oauth) {
options.oauth = JSON.parse(oauth);
};
}
var mountPath = process.env.PARSE_SERVER_MOUNT_PATH || "/";
var api = new ParseServer(options);
app.use(mountPath, api);
var port = process.env.PORT || 1337;
app.listen(port, function() {
console.log('parse-server-example running on http://localhost:'+ port + mountPath);
});
require("../lib/cli/parse-server");
Executable
+163
View File
@@ -0,0 +1,163 @@
#!/bin/sh
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
BOLD='\033[1m'
CHECK="${GREEN}\xE2\x9C\x93${NC}"
DEFAULT_MONGODB_URI='mongodb://localhost:127.0.0.1:27017/parse'
confirm() {
DEFAULT=$1;
shift
printf "$@"
read -r YN
if [ "$YN" = "" ] && [ "$DEFAULT" = 'N' ]; then
exit 1
elif [ "$YN" != "" ] && [ "$YN" != "y" ] && [ "$YN" != "Y" ]; then
echo 'Bye Bye!'
exit 1
fi
}
genstring() {
local l=$1
[ "$l" == "" ] && l=40
LC_ALL=C tr -dc A-Za-z0-9 < /dev/urandom | head -c ${l}
}
check_node() {
node=`which node 2>&1`
ret=$?
if [ $ret -eq 0 ] && [ -x "$node" ]; then
echo "${CHECK} node:" $(node -v)
(exit 0)
else
echo "parse-server cannot be installed without Node.js." >&2
exit 1
fi
}
check_npm() {
npm=`which npm 2>&1`
ret=$?
if [ $ret -eq 0 ] && [ -x "$npm" ]; then
echo "${CHECK} npm:" $(npm -v)
(exit 0)
else
echo "parse-server cannot be installed without npm." >&2
exit 1
fi
}
echo ''
echo 'This will setup parse-server in the current directory'
confirm 'Y' 'Do you want to continue? (Y/n): '
check_node
check_npm
echo "Setting up parse-server in $PWD"
if [ -f './package.json' ]; then
echo "\n${RED}package.json exists${NC}"
confirm 'N' "Do you want to continue? \n${RED}this will erase your configuration${NC} (y/N): "
fi
if [ -f 'config.json' ]; then
echo "\n${RED}config.json exists${NC}"
confirm 'N' "Do you want to continue? \n${RED}this will erase your configuration${NC} (y/N): "
fi
APP_NAME=''
i=0
while [ "$APP_NAME" = "" ]
do
[[ $i != 0 ]] && printf "${RED}An application name is required!${NC}\n"
printf 'Enter your Application Name: '
read -r APP_NAME
i=$(($i+1))
done
printf 'Enter your appId (leave empty to generate): '
read -r APP_ID
[[ $APP_ID = '' ]] && APP_ID=$(genstring) && printf "\n$APP_ID\n\n"
printf 'Enter your masterKey (leave empty to generate): '
read -r MASTER_KEY
[[ $MASTER_KEY = '' ]] && MASTER_KEY=$(genstring) && printf "\n$MASTER_KEY\n\n"
printf "Enter your mongodbURI (%s): " $DEFAULT_MONGODB_URI
read -r MONGODB_URI
[[ $MONGODB_URI = '' ]] && MONGODB_URI="$DEFAULT_MONGODB_URI"
cat > ./config.json << EOF
{
"appId": "$APP_ID",
"masterKey": "$MASTER_KEY",
"appName": "$APP_NAME",
"cloud": "./cloud/main",
"mongodbURI": "$MONGODB_URI"
}
EOF
echo "${CHECK} Created config.json"
# Make a proper npm app name
NPM_APP_NAME=$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
cat > ./package.json << EOF
{
"name": "$NPM_APP_NAME",
"scripts": {
"start": "parse-server ./config.json"
},
"dependencies": {
"parse-server": "^2.0.0"
}
}
EOF
echo "${CHECK} Created package.json"
if [ -d "./cloud/" ]; then
echo "${CHECK} cloud/ exists"
else
mkdir -p ./cloud
echo "${CHECK} Created cloud/"
fi
if [ -e "./cloud/main.js" ]; then
echo "${CHECK} cloud/main.js exists"
else
echo "${CHECK} Created cloud/main.js"
cat > ./cloud/main.js << EOF
// Cloud Code entry point
EOF
fi
if [ -d "./public/" ]; then
echo "${CHECK} public/ exists"
else
mkdir -p ./public
echo "${CHECK} Created public/"
fi
echo "\n${CHECK} running npm install\n"
npm install
CURL_CMD=$(cat << EOF
curl -X POST -H 'X-Parse-Application-Id: ${APP_ID}' \\
-H 'Content-Type: application/json' \\
-d '{"foo":"bar"}' http://localhost:1337/parse/classes/TestObject
EOF)
echo "\n${CHECK} Happy Parsing!\n\n"
echo "${CHECK} Make sure you have ${BOLD}mongo${NC} listening on ${BOLD}${MONGODB_URI}${NC}"
echo "${CHECK} start parse-server by running ${BOLD}npm start${NC}"
echo "${CHECK} Test your setup with:\n\n${CURL_CMD}\n"
+38 -11
View File
@@ -1,50 +1,77 @@
{
"name": "parse-server",
"version": "2.1.0",
"version": "2.2.6",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
"type": "git",
"url": "https://github.com/ParsePlatform/parse-server"
},
"files": [
"bin/",
"lib/",
"public_html/",
"views/",
"LICENSE",
"PATENTS",
"README.md"
],
"license": "BSD-3-Clause",
"dependencies": {
"apn": "^1.7.5",
"aws-sdk": "~2.2.33",
"babel-polyfill": "^6.5.0",
"babel-runtime": "^6.5.0",
"bcrypt-nodejs": "0.0.3",
"body-parser": "^1.14.2",
"colors": "^1.1.2",
"commander": "^2.9.0",
"deepcopy": "^0.6.1",
"express": "^4.13.4",
"intersect": "^1.0.1",
"lru-cache": "^4.0.0",
"mailgun-js": "^0.7.7",
"mime": "^1.3.4",
"mongodb": "~2.1.0",
"multer": "^1.1.0",
"node-gcm": "^0.14.0",
"parse": "^1.7.0",
"parse": "^1.8.0",
"parse-server-fs-adapter": "^1.0.0",
"parse-server-gcs-adapter": "^1.0.0",
"parse-server-push-adapter": "^1.0.0",
"parse-server-s3-adapter": "^1.0.0",
"parse-server-simple-mailgun-adapter": "^1.0.0",
"redis": "^2.5.0-1",
"request": "^2.65.0",
"winston": "^2.1.1"
"tv4": "^1.2.7",
"winston": "^2.1.1",
"winston-daily-rotate-file": "^1.0.1",
"ws": "^1.0.1"
},
"devDependencies": {
"babel-cli": "^6.5.1",
"babel-core": "^6.5.1",
"babel-istanbul": "^0.6.0",
"babel-plugin-syntax-flow": "^6.5.0",
"babel-plugin-transform-flow-strip-types": "^6.5.0",
"babel-preset-es2015": "^6.5.0",
"babel-preset-stage-0": "^6.5.0",
"babel-register": "^6.5.1",
"codecov": "^1.0.1",
"cross-env": "^1.0.7",
"deep-diff": "^0.3.3",
"flow-bin": "^0.22.0",
"gaze": "^0.5.2",
"jasmine": "^2.3.2",
"mongodb-runner": "^3.1.15",
"mongodb-runner": "3.1.15",
"nodemon": "^1.8.1"
},
"scripts": {
"dev": "npm run build && bin/dev",
"dev": "npm run build && node bin/dev",
"build": "./node_modules/.bin/babel src/ -d lib/",
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.0.8} ./node_modules/.bin/mongodb-runner start",
"test": "cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node ./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/** ./node_modules/jasmine/bin/jasmine.js",
"posttest": "mongodb-runner stop",
"start": "./bin/parse-server",
"test": "cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node $COVERAGE_OPTION ./node_modules/jasmine/bin/jasmine.js",
"test:win": "npm run pretest && cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node ./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/** ./node_modules/jasmine/bin/jasmine.js && npm run posttest",
"posttest": "./node_modules/.bin/mongodb-runner stop",
"coverage": "cross-env COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**' npm test",
"start": "node ./bin/parse-server",
"prepublish": "npm run build"
},
"engines": {
+43
View File
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<!-- This page is displayed when someone navigates to a verify email or reset password link
but their security token is wrong. This can either mean the user has clicked on a
stale link (i.e. re-click on a password reset link after resetting their password) or
(rarely) this could be a sign of a malicious user trying to tamper with your app.
-->
<html>
<head>
<title>Invalid Link</title>
<style type='text/css'>
.container {
border-width: 0px;
display: block;
font: inherit;
font-family: 'Helvetica Neue', Helvetica;
font-size: 16px;
height: 30px;
line-height: 16px;
margin: 45px 0px 0px 45px;
padding: 0px 8px 0px 8px;
position: relative;
vertical-align: baseline;
}
h1, h2, h3, h4, h5 {
color: #0067AB;
display: block;
font: inherit;
font-family: 'Open Sans', 'Helvetica Neue', Helvetica;
font-size: 30px;
font-weight: 600;
height: 30px;
line-height: 30px;
margin: 0 0 15px 0;
padding: 0 0 0 0;
}
</style>
<body>
<div class="container">
<h1>Invalid Link</h1>
</div>
</body>
</html>
+27
View File
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<!-- This page is displayed whenever someone has successfully reset their password.
Pro and Enterprise accounts may edit this page and tell Parse to use that custom
version in their Parse app. See the App Settigns page for more information.
This page will be called with the query param 'username'
-->
<head>
<title>Password Reset</title>
<style type='text/css'>
h1 {
color: #0067AB;
display: block;
font: inherit;
font-family: 'Open Sans', 'Helvetica Neue', Helvetica;
font-size: 30px;
font-weight: 600;
height: 30px;
line-height: 30px;
margin: 45px 0px 0px 45px;
padding: 0px 8px 0px 8px;
}
</style>
<body>
<h1>Successfully updated your password!</h1>
</body>
</html>
+27
View File
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<!-- This page is displayed whenever someone has successfully reset their password.
Pro and Enterprise accounts may edit this page and tell Parse to use that custom
version in their Parse app. See the App Settigns page for more information.
This page will be called with the query param 'username'
-->
<head>
<title>Email Verification</title>
<style type='text/css'>
h1 {
color: #0067AB;
display: block;
font: inherit;
font-family: 'Open Sans', 'Helvetica Neue', Helvetica;
font-size: 30px;
font-weight: 600;
height: 30px;
line-height: 30px;
margin: 45px 0px 0px 45px;
padding: 0px 8px 0px 8px;
}
</style>
<body>
<h1>Successfully verified your email!</h1>
</body>
</html>
-307
View File
@@ -1,307 +0,0 @@
var APNS = require('../src/APNS');
describe('APNS', () => {
it('can initialize with single cert', (done) => {
var args = {
cert: 'prodCert.pem',
key: 'prodKey.pem',
production: true,
bundleId: 'bundleId'
}
var apns = new APNS(args);
expect(apns.conns.length).toBe(1);
var apnsConnection = apns.conns[0];
expect(apnsConnection.index).toBe(0);
expect(apnsConnection.bundleId).toBe(args.bundleId);
// TODO: Remove this checking onec we inject APNS
var prodApnsOptions = apnsConnection.options;
expect(prodApnsOptions.cert).toBe(args.cert);
expect(prodApnsOptions.key).toBe(args.key);
expect(prodApnsOptions.production).toBe(args.production);
done();
});
it('can initialize with multiple certs', (done) => {
var args = [
{
cert: 'devCert.pem',
key: 'devKey.pem',
production: false,
bundleId: 'bundleId'
},
{
cert: 'prodCert.pem',
key: 'prodKey.pem',
production: true,
bundleId: 'bundleIdAgain'
}
]
var apns = new APNS(args);
expect(apns.conns.length).toBe(2);
var devApnsConnection = apns.conns[1];
expect(devApnsConnection.index).toBe(1);
var devApnsOptions = devApnsConnection.options;
expect(devApnsOptions.cert).toBe(args[0].cert);
expect(devApnsOptions.key).toBe(args[0].key);
expect(devApnsOptions.production).toBe(args[0].production);
expect(devApnsConnection.bundleId).toBe(args[0].bundleId);
var prodApnsConnection = apns.conns[0];
expect(prodApnsConnection.index).toBe(0);
// TODO: Remove this checking onec we inject APNS
var prodApnsOptions = prodApnsConnection.options;
expect(prodApnsOptions.cert).toBe(args[1].cert);
expect(prodApnsOptions.key).toBe(args[1].key);
expect(prodApnsOptions.production).toBe(args[1].production);
expect(prodApnsOptions.bundleId).toBe(args[1].bundleId);
done();
});
it('can generate APNS notification', (done) => {
//Mock request data
var data = {
'alert': 'alert',
'badge': 100,
'sound': 'test',
'content-available': 1,
'category': 'INVITE_CATEGORY',
'key': 'value',
'keyAgain': 'valueAgain'
};
var expirationTime = 1454571491354
var notification = APNS.generateNotification(data, expirationTime);
expect(notification.alert).toEqual(data.alert);
expect(notification.badge).toEqual(data.badge);
expect(notification.sound).toEqual(data.sound);
expect(notification.contentAvailable).toEqual(1);
expect(notification.category).toEqual(data.category);
expect(notification.payload).toEqual({
'key': 'value',
'keyAgain': 'valueAgain'
});
expect(notification.expiry).toEqual(expirationTime);
done();
});
it('can choose conns for device without appIdentifier', (done) => {
// Mock conns
var conns = [
{
bundleId: 'bundleId'
},
{
bundleId: 'bundleIdAgain'
}
];
// Mock device
var device = {};
var qualifiedConns = APNS.chooseConns(conns, device);
expect(qualifiedConns).toEqual([0, 1]);
done();
});
it('can choose conns for device with valid appIdentifier', (done) => {
// Mock conns
var conns = [
{
bundleId: 'bundleId'
},
{
bundleId: 'bundleIdAgain'
}
];
// Mock device
var device = {
appIdentifier: 'bundleId'
};
var qualifiedConns = APNS.chooseConns(conns, device);
expect(qualifiedConns).toEqual([0]);
done();
});
it('can choose conns for device with invalid appIdentifier', (done) => {
// Mock conns
var conns = [
{
bundleId: 'bundleId'
},
{
bundleId: 'bundleIdAgain'
}
];
// Mock device
var device = {
appIdentifier: 'invalid'
};
var qualifiedConns = APNS.chooseConns(conns, device);
expect(qualifiedConns).toEqual([]);
done();
});
it('can handle transmission error when notification is not in cache or device is missing', (done) => {
// Mock conns
var conns = [];
var errorCode = 1;
var notification = undefined;
var device = {};
APNS.handleTransmissionError(conns, errorCode, notification, device);
var notification = {};
var device = undefined;
APNS.handleTransmissionError(conns, errorCode, notification, device);
done();
});
it('can handle transmission error when there are other qualified conns', (done) => {
// Mock conns
var conns = [
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId1'
},
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId1'
},
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId2'
},
];
var errorCode = 1;
var notification = {};
var apnDevice = {
connIndex: 0,
appIdentifier: 'bundleId1'
};
APNS.handleTransmissionError(conns, errorCode, notification, apnDevice);
expect(conns[0].pushNotification).not.toHaveBeenCalled();
expect(conns[1].pushNotification).toHaveBeenCalled();
expect(conns[2].pushNotification).not.toHaveBeenCalled();
done();
});
it('can handle transmission error when there is no other qualified conns', (done) => {
// Mock conns
var conns = [
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId1'
},
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId1'
},
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId1'
},
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId2'
},
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId1'
}
];
var errorCode = 1;
var notification = {};
var apnDevice = {
connIndex: 2,
appIdentifier: 'bundleId1'
};
APNS.handleTransmissionError(conns, errorCode, notification, apnDevice);
expect(conns[0].pushNotification).not.toHaveBeenCalled();
expect(conns[1].pushNotification).not.toHaveBeenCalled();
expect(conns[2].pushNotification).not.toHaveBeenCalled();
expect(conns[3].pushNotification).not.toHaveBeenCalled();
expect(conns[4].pushNotification).toHaveBeenCalled();
done();
});
it('can handle transmission error when device has no appIdentifier', (done) => {
// Mock conns
var conns = [
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId1'
},
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId2'
},
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId3'
},
];
var errorCode = 1;
var notification = {};
var apnDevice = {
connIndex: 1,
};
APNS.handleTransmissionError(conns, errorCode, notification, apnDevice);
expect(conns[0].pushNotification).not.toHaveBeenCalled();
expect(conns[1].pushNotification).not.toHaveBeenCalled();
expect(conns[2].pushNotification).toHaveBeenCalled();
done();
});
it('can send APNS notification', (done) => {
var args = {
cert: 'prodCert.pem',
key: 'prodKey.pem',
production: true,
bundleId: 'bundleId'
}
var apns = new APNS(args);
var conn = {
pushNotification: jasmine.createSpy('send'),
bundleId: 'bundleId'
};
apns.conns = [ conn ];
// Mock data
var expirationTime = 1454571491354
var data = {
'expiration_time': expirationTime,
'data': {
'alert': 'alert'
}
}
// Mock devices
var devices = [
{
deviceToken: '112233',
appIdentifier: 'bundleId'
}
];
var promise = apns.send(data, devices);
expect(conn.pushNotification).toHaveBeenCalled();
var args = conn.pushNotification.calls.first().args;
var notification = args[0];
expect(notification.alert).toEqual(data.data.alert);
expect(notification.expiry).toEqual(data['expiration_time']);
var apnDevice = args[1]
expect(apnDevice.connIndex).toEqual(0);
expect(apnDevice.appIdentifier).toEqual('bundleId');
done();
});
});
+87
View File
@@ -0,0 +1,87 @@
var AdaptableController = require("../src/Controllers/AdaptableController").AdaptableController;
var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default;
var FilesController = require("../src/Controllers/FilesController").FilesController;
var MockController = function(options) {
AdaptableController.call(this, options);
}
MockController.prototype = Object.create(AdaptableController.prototype);
MockController.prototype.constructor = AdaptableController;
describe("AdaptableController", ()=>{
it("should use the provided adapter", (done) => {
var adapter = new FilesAdapter();
var controller = new FilesController(adapter);
expect(controller.adapter).toBe(adapter);
// make sure _adapter is private
expect(controller._adapter).toBe(undefined);
// Override _adapter is not doing anything
controller._adapter = "Hello";
expect(controller.adapter).toBe(adapter);
done();
});
it("should throw when creating a new mock controller", (done) => {
var adapter = new FilesAdapter();
expect(() => {
new MockController(adapter);
}).toThrow();
done();
});
it("should fail setting the wrong adapter to the controller", (done) => {
function WrongAdapter() {};
var adapter = new FilesAdapter();
var controller = new FilesController(adapter);
var otherAdapter = new WrongAdapter();
expect(() => {
controller.adapter = otherAdapter;
}).toThrow();
done();
});
it("should fail to instantiate a controller with wrong adapter", (done) => {
function WrongAdapter() {};
var adapter = new WrongAdapter();
expect(() => {
new FilesController(adapter);
}).toThrow();
done();
});
it("should fail to instantiate a controller without an adapter", (done) => {
expect(() => {
new FilesController();
}).toThrow();
done();
});
it("should accept an object adapter", (done) => {
var adapter = {
createFile: function(config, filename, data) { },
deleteFile: function(config, filename) { },
getFileData: function(config, filename) { },
getFileLocation: function(config, filename) { },
}
expect(() => {
new FilesController(adapter);
}).not.toThrow();
done();
});
it("should accept an object adapter", (done) => {
function AGoodAdapter() {};
AGoodAdapter.prototype.createFile = function(config, filename, data) { };
AGoodAdapter.prototype.deleteFile = function(config, filename) { };
AGoodAdapter.prototype.getFileData = function(config, filename) { };
AGoodAdapter.prototype.getFileLocation = function(config, filename) { };
var adapter = new AGoodAdapter();
expect(() => {
new FilesController(adapter);
}).not.toThrow();
done();
});
});
+126
View File
@@ -0,0 +1,126 @@
var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter;
var FilesAdapter = require("parse-server-fs-adapter").default;
var S3Adapter = require("parse-server-s3-adapter").default;
var GCSAdapter = require("parse-server-gcs-adapter").default;
var ParsePushAdapter = require("parse-server-push-adapter").default;
describe("AdapterLoader", ()=>{
it("should instantiate an adapter from string in object", (done) => {
var adapterPath = require('path').resolve("./spec/MockAdapter");
var adapter = loadAdapter({
adapter: adapterPath,
options: {
key: "value",
foo: "bar"
}
});
expect(adapter instanceof Object).toBe(true);
expect(adapter.options.key).toBe("value");
expect(adapter.options.foo).toBe("bar");
done();
});
it("should instantiate an adapter from string", (done) => {
var adapterPath = require('path').resolve("./spec/MockAdapter");
var adapter = loadAdapter(adapterPath);
expect(adapter instanceof Object).toBe(true);
done();
});
it("should instantiate an adapter from string that is module", (done) => {
var adapterPath = require('path').resolve("./src/Adapters/Files/FilesAdapter");
var adapter = loadAdapter({
adapter: adapterPath
});
expect(typeof adapter).toBe('object');
expect(typeof adapter.createFile).toBe('function');
expect(typeof adapter.deleteFile).toBe('function');
expect(typeof adapter.getFileData).toBe('function');
expect(typeof adapter.getFileLocation).toBe('function');
done();
});
it("should instantiate an adapter from function/Class", (done) => {
var adapter = loadAdapter({
adapter: FilesAdapter
});
expect(adapter instanceof FilesAdapter).toBe(true);
done();
});
it("should instantiate the default adapter from Class", (done) => {
var adapter = loadAdapter(null, FilesAdapter);
expect(adapter instanceof FilesAdapter).toBe(true);
done();
});
it("should use the default adapter", (done) => {
var defaultAdapter = new FilesAdapter();
var adapter = loadAdapter(null, defaultAdapter);
expect(adapter instanceof FilesAdapter).toBe(true);
done();
});
it("should use the provided adapter", (done) => {
var originalAdapter = new FilesAdapter();
var adapter = loadAdapter(originalAdapter);
expect(adapter).toBe(originalAdapter);
done();
});
it("should fail loading an improperly configured adapter", (done) => {
var Adapter = function(options) {
if (!options.foo) {
throw "foo is required for that adapter";
}
}
var adapterOptions = {
param: "key",
doSomething: function() {}
};
expect(() => {
var adapter = loadAdapter(adapterOptions, Adapter);
expect(adapter).toEqual(adapterOptions);
}).not.toThrow("foo is required for that adapter");
done();
});
it("should load push adapter from options", (done) => {
var options = {
ios: {
bundleId: 'bundle.id'
}
}
expect(() => {
var adapter = loadAdapter(undefined, ParsePushAdapter, options);
expect(adapter.constructor).toBe(ParsePushAdapter);
expect(adapter).not.toBe(undefined);
}).not.toThrow();
done();
});
it("should load S3Adapter from direct passing", (done) => {
var s3Adapter = new S3Adapter("key", "secret", "bucket")
expect(() => {
var adapter = loadAdapter(s3Adapter, FilesAdapter);
expect(adapter).toBe(s3Adapter);
}).not.toThrow();
done();
})
it("should load GCSAdapter from direct passing", (done) => {
var gcsAdapter = new GCSAdapter("projectId", "path/to/keyfile", "bucket")
expect(() => {
var adapter = loadAdapter(gcsAdapter, FilesAdapter);
expect(adapter).toBe(gcsAdapter);
}).not.toThrow();
done();
})
});
+139
View File
@@ -0,0 +1,139 @@
'use strict';
var commander = require("../src/cli/utils/commander").default;
var definitions = {
"arg0": "PROGRAM_ARG_0",
"arg1": {
env: "PROGRAM_ARG_1",
required: true
},
"arg2": {
env: "PROGRAM_ARG_2",
action: function(value) {
var value = parseInt(value);
if (!Number.isInteger(value)) {
throw "arg2 is invalid";
}
return value;
}
},
"arg3": {},
"arg4": {
default: "arg4Value"
}
}
describe("commander additions", () => {
afterEach((done) => {
commander.options = [];
delete commander.arg0;
delete commander.arg1;
delete commander.arg2;
delete commander.arg3;
delete commander.arg4;
done();
})
it("should load properly definitions from args", (done) => {
commander.loadDefinitions(definitions);
commander.parse(["node","./CLI.spec.js","--arg0", "arg0Value", "--arg1", "arg1Value", "--arg2", "2", "--arg3", "some"]);
expect(commander.arg0).toEqual("arg0Value");
expect(commander.arg1).toEqual("arg1Value");
expect(commander.arg2).toEqual(2);
expect(commander.arg3).toEqual("some");
expect(commander.arg4).toEqual("arg4Value");
done();
});
it("should load properly definitions from env", (done) => {
commander.loadDefinitions(definitions);
commander.parse([], {
"PROGRAM_ARG_0": "arg0ENVValue",
"PROGRAM_ARG_1": "arg1ENVValue",
"PROGRAM_ARG_2": "3",
});
expect(commander.arg0).toEqual("arg0ENVValue");
expect(commander.arg1).toEqual("arg1ENVValue");
expect(commander.arg2).toEqual(3);
expect(commander.arg4).toEqual("arg4Value");
done();
});
it("should load properly use args over env", (done) => {
commander.loadDefinitions(definitions);
commander.parse(["node","./CLI.spec.js","--arg0", "arg0Value", "--arg4", "anotherArg4"], {
"PROGRAM_ARG_0": "arg0ENVValue",
"PROGRAM_ARG_1": "arg1ENVValue",
"PROGRAM_ARG_2": "4",
});
expect(commander.arg0).toEqual("arg0Value");
expect(commander.arg1).toEqual("arg1ENVValue");
expect(commander.arg2).toEqual(4);
expect(commander.arg4).toEqual("anotherArg4");
done();
});
it("should fail in action as port is invalid", (done) => {
commander.loadDefinitions(definitions);
expect(()=> {
commander.parse(["node","./CLI.spec.js","--arg0", "arg0Value"], {
"PROGRAM_ARG_0": "arg0ENVValue",
"PROGRAM_ARG_1": "arg1ENVValue",
"PROGRAM_ARG_2": "hello",
});
}).toThrow("arg2 is invalid");
done();
});
it("should not override config.json", (done) => {
commander.loadDefinitions(definitions);
commander.parse(["node","./CLI.spec.js","--arg0", "arg0Value", "./spec/configs/CLIConfig.json"], {
"PROGRAM_ARG_0": "arg0ENVValue",
"PROGRAM_ARG_1": "arg1ENVValue",
});
let options = commander.getOptions();
expect(options.arg2).toBe(8888);
expect(options.arg3).toBe("hello"); //config value
expect(options.arg4).toBe('/1');
done();
});
it("should fail with invalid values in JSON", (done) => {
commander.loadDefinitions(definitions);
expect(() => {
commander.parse(["node","./CLI.spec.js","--arg0", "arg0Value", "./spec/configs/CLIConfigFail.json"], {
"PROGRAM_ARG_0": "arg0ENVValue",
"PROGRAM_ARG_1": "arg1ENVValue",
});
}).toThrow("arg2 is invalid")
done();
});
it("should fail when too many apps are set", (done) => {
commander.loadDefinitions(definitions);
expect(() => {
commander.parse(["node","./CLI.spec.js","./spec/configs/CLIConfigFailTooManyApps.json"]);
}).toThrow("Multiple apps are not supported")
done();
});
it("should load config from apps", (done) => {
commander.loadDefinitions(definitions);
commander.parse(["node", "./CLI.spec.js", "./spec/configs/CLIConfigApps.json"]);
let options = commander.getOptions();
expect(options.arg1).toBe("my_app");
expect(options.arg2).toBe(8888);
expect(options.arg3).toBe("hello"); //config value
expect(options.arg4).toBe('/1');
done();
});
it("should fail when passing an invalid arguement", (done) => {
commander.loadDefinitions(definitions);
expect(() => {
commander.parse(["node", "./CLI.spec.js", "./spec/configs/CLIConfigUnknownArg.json"]);
}).toThrow('error: unknown option myArg')
done();
});
});
+290
View File
@@ -0,0 +1,290 @@
var Client = require('../src/LiveQuery/Client').Client;
var ParseWebSocket = require('../src/LiveQuery/ParseWebSocketServer').ParseWebSocket;
describe('Client', function() {
it('can be initialized', function() {
var parseWebSocket = new ParseWebSocket({});
var client = new Client(1, parseWebSocket);
expect(client.id).toBe(1);
expect(client.parseWebSocket).toBe(parseWebSocket);
expect(client.subscriptionInfos.size).toBe(0);
});
it('can push response', function() {
var parseWebSocket = {
send: jasmine.createSpy('send')
};
Client.pushResponse(parseWebSocket, 'message');
expect(parseWebSocket.send).toHaveBeenCalledWith('message');
});
it('can push error', function() {
var parseWebSocket = {
send: jasmine.createSpy('send')
};
Client.pushError(parseWebSocket, 1, 'error', true);
var lastCall = parseWebSocket.send.calls.first();
var messageJSON = JSON.parse(lastCall.args[0]);
expect(messageJSON.op).toBe('error');
expect(messageJSON.error).toBe('error');
expect(messageJSON.code).toBe(1);
expect(messageJSON.reconnect).toBe(true);
});
it('can add subscription information', function() {
var subscription = {};
var fields = ['test'];
var subscriptionInfo = {
subscription: subscription,
fields: fields
}
var client = new Client(1, {});
client.addSubscriptionInfo(1, subscriptionInfo);
expect(client.subscriptionInfos.size).toBe(1);
expect(client.subscriptionInfos.get(1)).toBe(subscriptionInfo);
});
it('can get subscription information', function() {
var subscription = {};
var fields = ['test'];
var subscriptionInfo = {
subscription: subscription,
fields: fields
}
var client = new Client(1, {});
client.addSubscriptionInfo(1, subscriptionInfo);
var subscriptionInfoAgain = client.getSubscriptionInfo(1);
expect(subscriptionInfoAgain).toBe(subscriptionInfo);
});
it('can delete subscription information', function() {
var subscription = {};
var fields = ['test'];
var subscriptionInfo = {
subscription: subscription,
fields: fields
}
var client = new Client(1, {});
client.addSubscriptionInfo(1, subscriptionInfo);
client.deleteSubscriptionInfo(1);
expect(client.subscriptionInfos.size).toBe(0);
});
it('can generate ParseObject JSON with null selected field', function() {
var parseObjectJSON = {
key : 'value',
className: 'test',
objectId: 'test',
updatedAt: '2015-12-07T21:27:13.746Z',
createdAt: '2015-12-07T21:27:13.746Z',
ACL: 'test',
};
var client = new Client(1, {});
expect(client._toJSONWithFields(parseObjectJSON, null)).toBe(parseObjectJSON);
});
it('can generate ParseObject JSON with undefined selected field', function() {
var parseObjectJSON = {
key : 'value',
className: 'test',
objectId: 'test',
updatedAt: '2015-12-07T21:27:13.746Z',
createdAt: '2015-12-07T21:27:13.746Z',
ACL: 'test',
};
var client = new Client(1, {});
expect(client._toJSONWithFields(parseObjectJSON, undefined)).toBe(parseObjectJSON);
});
it('can generate ParseObject JSON with selected fields', function() {
var parseObjectJSON = {
key : 'value',
className: 'test',
objectId: 'test',
updatedAt: '2015-12-07T21:27:13.746Z',
createdAt: '2015-12-07T21:27:13.746Z',
ACL: 'test',
test: 'test'
};
var client = new Client(1, {});
expect(client._toJSONWithFields(parseObjectJSON, ['test'])).toEqual({
className: 'test',
objectId: 'test',
updatedAt: '2015-12-07T21:27:13.746Z',
createdAt: '2015-12-07T21:27:13.746Z',
ACL: 'test',
test: 'test'
});
});
it('can generate ParseObject JSON with nonexistent selected fields', function() {
var parseObjectJSON = {
key : 'value',
className: 'test',
objectId: 'test',
updatedAt: '2015-12-07T21:27:13.746Z',
createdAt: '2015-12-07T21:27:13.746Z',
ACL: 'test',
test: 'test'
};
var client = new Client(1, {});
var limitedParseObject = client._toJSONWithFields(parseObjectJSON, ['name']);
expect(limitedParseObject).toEqual({
className: 'test',
objectId: 'test',
updatedAt: '2015-12-07T21:27:13.746Z',
createdAt: '2015-12-07T21:27:13.746Z',
ACL: 'test',
});
expect('name' in limitedParseObject).toBe(false);
});
it('can push connect response', function() {
var parseWebSocket = {
send: jasmine.createSpy('send')
};
var client = new Client(1, parseWebSocket);
client.pushConnect();
var lastCall = parseWebSocket.send.calls.first();
var messageJSON = JSON.parse(lastCall.args[0]);
expect(messageJSON.op).toBe('connected');
expect(messageJSON.clientId).toBe(1);
});
it('can push subscribe response', function() {
var parseWebSocket = {
send: jasmine.createSpy('send')
};
var client = new Client(1, parseWebSocket);
client.pushSubscribe(2);
var lastCall = parseWebSocket.send.calls.first();
var messageJSON = JSON.parse(lastCall.args[0]);
expect(messageJSON.op).toBe('subscribed');
expect(messageJSON.clientId).toBe(1);
expect(messageJSON.requestId).toBe(2);
});
it('can push unsubscribe response', function() {
var parseWebSocket = {
send: jasmine.createSpy('send')
};
var client = new Client(1, parseWebSocket);
client.pushUnsubscribe(2);
var lastCall = parseWebSocket.send.calls.first();
var messageJSON = JSON.parse(lastCall.args[0]);
expect(messageJSON.op).toBe('unsubscribed');
expect(messageJSON.clientId).toBe(1);
expect(messageJSON.requestId).toBe(2);
});
it('can push create response', function() {
var parseObjectJSON = {
key : 'value',
className: 'test',
objectId: 'test',
updatedAt: '2015-12-07T21:27:13.746Z',
createdAt: '2015-12-07T21:27:13.746Z',
ACL: 'test',
test: 'test'
};
var parseWebSocket = {
send: jasmine.createSpy('send')
};
var client = new Client(1, parseWebSocket);
client.pushCreate(2, parseObjectJSON);
var lastCall = parseWebSocket.send.calls.first();
var messageJSON = JSON.parse(lastCall.args[0]);
expect(messageJSON.op).toBe('create');
expect(messageJSON.clientId).toBe(1);
expect(messageJSON.requestId).toBe(2);
expect(messageJSON.object).toEqual(parseObjectJSON);
});
it('can push enter response', function() {
var parseObjectJSON = {
key : 'value',
className: 'test',
objectId: 'test',
updatedAt: '2015-12-07T21:27:13.746Z',
createdAt: '2015-12-07T21:27:13.746Z',
ACL: 'test',
test: 'test'
};
var parseWebSocket = {
send: jasmine.createSpy('send')
};
var client = new Client(1, parseWebSocket);
client.pushEnter(2, parseObjectJSON);
var lastCall = parseWebSocket.send.calls.first();
var messageJSON = JSON.parse(lastCall.args[0]);
expect(messageJSON.op).toBe('enter');
expect(messageJSON.clientId).toBe(1);
expect(messageJSON.requestId).toBe(2);
expect(messageJSON.object).toEqual(parseObjectJSON);
});
it('can push update response', function() {
var parseObjectJSON = {
key : 'value',
className: 'test',
objectId: 'test',
updatedAt: '2015-12-07T21:27:13.746Z',
createdAt: '2015-12-07T21:27:13.746Z',
ACL: 'test',
test: 'test'
};
var parseWebSocket = {
send: jasmine.createSpy('send')
};
var client = new Client(1, parseWebSocket);
client.pushUpdate(2, parseObjectJSON);
var lastCall = parseWebSocket.send.calls.first();
var messageJSON = JSON.parse(lastCall.args[0]);
expect(messageJSON.op).toBe('update');
expect(messageJSON.clientId).toBe(1);
expect(messageJSON.requestId).toBe(2);
expect(messageJSON.object).toEqual(parseObjectJSON);
});
it('can push leave response', function() {
var parseObjectJSON = {
key : 'value',
className: 'test',
objectId: 'test',
updatedAt: '2015-12-07T21:27:13.746Z',
createdAt: '2015-12-07T21:27:13.746Z',
ACL: 'test',
test: 'test'
};
var parseWebSocket = {
send: jasmine.createSpy('send')
};
var client = new Client(1, parseWebSocket);
client.pushLeave(2, parseObjectJSON);
var lastCall = parseWebSocket.send.calls.first();
var messageJSON = JSON.parse(lastCall.args[0]);
expect(messageJSON.op).toBe('leave');
expect(messageJSON.clientId).toBe(1);
expect(messageJSON.requestId).toBe(2);
expect(messageJSON.object).toEqual(parseObjectJSON);
});
});
+23
View File
@@ -0,0 +1,23 @@
'use strict';
let DatabaseAdapter = require('../src/DatabaseAdapter');
describe('DatabaseAdapter', () => {
it('options and URI are available to adapter', done => {
DatabaseAdapter.setAppDatabaseURI('optionsTest', 'mongodb://localhost:27017/optionsTest');
DatabaseAdapter.setAppDatabaseOptions('optionsTest', {foo: "bar"});
let optionsTestDatabaseConnection = DatabaseAdapter.getDatabaseConnection('optionsTest');
expect(optionsTestDatabaseConnection instanceof Object).toBe(true);
expect(optionsTestDatabaseConnection.adapter._options instanceof Object).toBe(true);
expect(optionsTestDatabaseConnection.adapter._options.foo).toBe("bar");
DatabaseAdapter.setAppDatabaseURI('noOptionsTest', 'mongodb://localhost:27017/noOptionsTest');
let noOptionsTestDatabaseConnection = DatabaseAdapter.getDatabaseConnection('noOptionsTest');
expect(noOptionsTestDatabaseConnection instanceof Object).toBe(true);
expect(noOptionsTestDatabaseConnection.adapter._options instanceof Object).toBe(false);
done();
});
});
+17
View File
@@ -0,0 +1,17 @@
'use strict';
let DatabaseController = require('../src/Controllers/DatabaseController');
let MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
describe('DatabaseController', () => {
it('can be constructed', done => {
let adapter = new MongoStorageAdapter('mongodb://localhost:27017/test');
let databaseController = new DatabaseController(adapter, {
collectionPrefix: 'test_'
});
databaseController.connect().then(done, error => {
console.log('error', error.stack);
fail();
});
});
});
+44
View File
@@ -0,0 +1,44 @@
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
describe('EventEmitterPubSub', function() {
it('can publish and subscribe', function() {
var publisher = EventEmitterPubSub.createPublisher();
var subscriber = EventEmitterPubSub.createSubscriber();
subscriber.subscribe('testChannel');
// Register mock checked for subscriber
var isChecked = false;
subscriber.on('message', function(channel, message) {
isChecked = true;
expect(channel).toBe('testChannel');
expect(message).toBe('testMessage');
});
publisher.publish('testChannel', 'testMessage');
// Make sure the callback is checked
expect(isChecked).toBe(true);
});
it('can unsubscribe', function() {
var publisher = EventEmitterPubSub.createPublisher();
var subscriber = EventEmitterPubSub.createSubscriber();
subscriber.subscribe('testChannel');
subscriber.unsubscribe('testChannel');
// Register mock checked for subscriber
var isCalled = false;
subscriber.on('message', function(channel, message) {
isCalled = true;
});
publisher.publish('testChannel', 'testMessage');
// Make sure the callback is not called
expect(isCalled).toBe(false);
});
it('can unsubscribe not subscribing channel', function() {
var subscriber = EventEmitterPubSub.createSubscriber();
// Make sure subscriber does not throw exception
subscriber.unsubscribe('testChannel');
});
});
-15
View File
@@ -1,15 +0,0 @@
var ExportAdapter = require('../src/ExportAdapter');
describe('ExportAdapter', () => {
it('can be constructed', (done) => {
var database = new ExportAdapter('mongodb://localhost:27017/test',
{
collectionPrefix: 'test_'
});
database.connect().then(done, (error) => {
console.log('error', error.stack);
fail();
});
});
});
+16 -35
View File
@@ -1,42 +1,22 @@
var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter;
var Parse = require('parse/node').Parse;
var request = require('request');
var fs = require('fs');
var LOGS_FOLDER = './test_logs/';
var deleteFolderRecursive = function(path) {
if( fs.existsSync(path) ) {
fs.readdirSync(path).forEach(function(file,index){
var curPath = path + "/" + file;
if(fs.lstatSync(curPath).isDirectory()) { // recurse
deleteFolderRecursive(curPath);
} else { // delete file
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(path);
}
};
describe('info logs', () => {
afterEach((done) => {
deleteFolderRecursive(LOGS_FOLDER);
done();
});
it("Verify INFO logs", (done) => {
var fileLoggerAdapter = new FileLoggerAdapter({
logsFolder: LOGS_FOLDER
});
var fileLoggerAdapter = new FileLoggerAdapter();
fileLoggerAdapter.info('testing info logs', () => {
fileLoggerAdapter.query({
size: 1,
level: 'info'
}, (results) => {
expect(results[0].message).toEqual('testing info logs');
done();
if(results.length == 0) {
fail('The adapter should return non-empty results');
done();
} else {
expect(results[0].message).toEqual('testing info logs');
done();
}
});
});
});
@@ -44,11 +24,6 @@ describe('info logs', () => {
describe('error logs', () => {
afterEach((done) => {
deleteFolderRecursive(LOGS_FOLDER);
done();
});
it("Verify ERROR logs", (done) => {
var fileLoggerAdapter = new FileLoggerAdapter();
fileLoggerAdapter.error('testing error logs', () => {
@@ -56,8 +31,14 @@ describe('error logs', () => {
size: 1,
level: 'error'
}, (results) => {
expect(results[0].message).toEqual('testing error logs');
done();
if(results.length == 0) {
fail('The adapter should return non-empty results');
done();
}
else {
expect(results[0].message).toEqual('testing error logs');
done();
}
});
});
});
+30
View File
@@ -0,0 +1,30 @@
var GridStoreAdapter = require("../src/Adapters/Files/GridStoreAdapter").GridStoreAdapter;
var Config = require("../src/Config");
var FilesController = require('../src/Controllers/FilesController').default;
// Small additional tests to improve overall coverage
describe("FilesController",() =>{
it("should properly expand objects", (done) => {
var config = new Config(Parse.applicationId);
var gridStoreAdapter = new GridStoreAdapter('mongodb://localhost:27017/parse');
var filesController = new FilesController(gridStoreAdapter)
var result = filesController.expandFilesInObject(config, function(){});
expect(result).toBeUndefined();
var fullFile = {
type: '__type',
url: "http://an.url"
}
var anObject = {
aFile: fullFile
}
filesController.expandFilesInObject(config, anObject);
expect(anObject.aFile.url).toEqual("http://an.url");
done();
})
});
-159
View File
@@ -1,159 +0,0 @@
var GCM = require('../src/GCM');
describe('GCM', () => {
it('can initialize', (done) => {
var args = {
apiKey: 'apiKey'
};
var gcm = new GCM(args);
expect(gcm.sender.key).toBe(args.apiKey);
done();
});
it('can throw on initializing with invalid args', (done) => {
var args = 123
expect(function() {
new GCM(args);
}).toThrow();
done();
});
it('can generate GCM Payload without expiration time', (done) => {
//Mock request data
var data = {
'alert': 'alert'
};
var pushId = 1;
var timeStamp = 1454538822113;
var timeStampISOStr = new Date(timeStamp).toISOString();
var payload = GCM.generateGCMPayload(data, pushId, timeStamp);
expect(payload.priority).toEqual('normal');
expect(payload.timeToLive).toEqual(undefined);
var dataFromPayload = payload.data;
expect(dataFromPayload.time).toEqual(timeStampISOStr);
expect(dataFromPayload['push_id']).toEqual(pushId);
var dataFromUser = JSON.parse(dataFromPayload.data);
expect(dataFromUser).toEqual(data);
done();
});
it('can generate GCM Payload with valid expiration time', (done) => {
//Mock request data
var data = {
'alert': 'alert'
};
var pushId = 1;
var timeStamp = 1454538822113;
var timeStampISOStr = new Date(timeStamp).toISOString();
var expirationTime = 1454538922113
var payload = GCM.generateGCMPayload(data, pushId, timeStamp, expirationTime);
expect(payload.priority).toEqual('normal');
expect(payload.timeToLive).toEqual(Math.floor((expirationTime - timeStamp) / 1000));
var dataFromPayload = payload.data;
expect(dataFromPayload.time).toEqual(timeStampISOStr);
expect(dataFromPayload['push_id']).toEqual(pushId);
var dataFromUser = JSON.parse(dataFromPayload.data);
expect(dataFromUser).toEqual(data);
done();
});
it('can generate GCM Payload with too early expiration time', (done) => {
//Mock request data
var data = {
'alert': 'alert'
};
var pushId = 1;
var timeStamp = 1454538822113;
var timeStampISOStr = new Date(timeStamp).toISOString();
var expirationTime = 1454538822112;
var payload = GCM.generateGCMPayload(data, pushId, timeStamp, expirationTime);
expect(payload.priority).toEqual('normal');
expect(payload.timeToLive).toEqual(0);
var dataFromPayload = payload.data;
expect(dataFromPayload.time).toEqual(timeStampISOStr);
expect(dataFromPayload['push_id']).toEqual(pushId);
var dataFromUser = JSON.parse(dataFromPayload.data);
expect(dataFromUser).toEqual(data);
done();
});
it('can generate GCM Payload with too late expiration time', (done) => {
//Mock request data
var data = {
'alert': 'alert'
};
var pushId = 1;
var timeStamp = 1454538822113;
var timeStampISOStr = new Date(timeStamp).toISOString();
var expirationTime = 2454538822113;
var payload = GCM.generateGCMPayload(data, pushId, timeStamp, expirationTime);
expect(payload.priority).toEqual('normal');
// Four week in second
expect(payload.timeToLive).toEqual(4 * 7 * 24 * 60 * 60);
var dataFromPayload = payload.data;
expect(dataFromPayload.time).toEqual(timeStampISOStr);
expect(dataFromPayload['push_id']).toEqual(pushId);
var dataFromUser = JSON.parse(dataFromPayload.data);
expect(dataFromUser).toEqual(data);
done();
});
it('can send GCM request', (done) => {
var gcm = new GCM({
apiKey: 'apiKey'
});
// Mock gcm sender
var sender = {
send: jasmine.createSpy('send')
};
gcm.sender = sender;
// Mock data
var expirationTime = 2454538822113;
var data = {
'expiration_time': expirationTime,
'data': {
'alert': 'alert'
}
}
// Mock devices
var devices = [
{
deviceToken: 'token'
}
];
gcm.send(data, devices);
expect(sender.send).toHaveBeenCalled();
var args = sender.send.calls.first().args;
// It is too hard to verify message of gcm library, we just verify tokens and retry times
expect(args[1].registrationTokens).toEqual(['token']);
expect(args[2]).toEqual(5);
done();
});
it('can slice devices', (done) => {
// Mock devices
var devices = [makeDevice(1), makeDevice(2), makeDevice(3), makeDevice(4)];
var chunkDevices = GCM.sliceDevices(devices, 3);
expect(chunkDevices).toEqual([
[makeDevice(1), makeDevice(2), makeDevice(3)],
[makeDevice(4)]
]);
done();
});
function makeDevice(deviceToken) {
return {
deviceToken: deviceToken
};
}
});
+260
View File
@@ -0,0 +1,260 @@
'use strict';
var httpRequest = require("../src/cloud-code/httpRequest"),
bodyParser = require('body-parser'),
express = require("express");
var port = 13371;
var httpRequestServer = "http://localhost:"+port;
var app = express();
app.use(bodyParser.json({ 'type': '*/*' }));
app.get("/hello", function(req, res){
res.json({response: "OK"});
});
app.get("/404", function(req, res){
res.status(404);
res.send("NO");
});
app.get("/301", function(req, res){
res.status(301);
res.location("/hello");
res.send();
});
app.post('/echo', function(req, res){
res.json(req.body);
});
app.get('/qs', function(req, res){
res.json(req.query);
});
app.listen(13371);
describe("httpRequest", () => {
it("should do /hello", (done) => {
httpRequest({
url: httpRequestServer+"/hello"
}).then(function(httpResponse){
expect(httpResponse.status).toBe(200);
expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}'));
expect(httpResponse.text).toEqual('{"response":"OK"}');
expect(httpResponse.data.response).toEqual("OK");
done();
}, function(){
fail("should not fail");
done();
})
});
it("should do /hello with callback and promises", (done) => {
var calls = 0;
httpRequest({
url: httpRequestServer+"/hello",
success: function() { calls++; },
error: function() { calls++; }
}).then(function(httpResponse){
expect(calls).toBe(1);
expect(httpResponse.status).toBe(200);
expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}'));
expect(httpResponse.text).toEqual('{"response":"OK"}');
expect(httpResponse.data.response).toEqual("OK");
done();
}, function(){
fail("should not fail");
done();
})
});
it("should do not follow redirects by default", (done) => {
httpRequest({
url: httpRequestServer+"/301"
}).then(function(httpResponse){
expect(httpResponse.status).toBe(301);
done();
}, function(){
fail("should not fail");
done();
})
});
it("should follow redirects when set", (done) => {
httpRequest({
url: httpRequestServer+"/301",
followRedirects: true
}).then(function(httpResponse){
expect(httpResponse.status).toBe(200);
expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}'));
expect(httpResponse.text).toEqual('{"response":"OK"}');
expect(httpResponse.data.response).toEqual("OK");
done();
}, function(){
fail("should not fail");
done();
})
});
it("should fail on 404", (done) => {
var calls = 0;
httpRequest({
url: httpRequestServer+"/404",
success: function() {
calls++;
fail("should not succeed");
done();
},
error: function(httpResponse) {
calls++;
expect(calls).toBe(1);
expect(httpResponse.status).toBe(404);
expect(httpResponse.buffer).toEqual(new Buffer('NO'));
expect(httpResponse.text).toEqual('NO');
expect(httpResponse.data).toBe(undefined);
done();
}
});
})
it("should fail on 404", (done) => {
httpRequest({
url: httpRequestServer+"/404",
}).then(function(httpResponse){
fail("should not succeed");
done();
}, function(httpResponse){
expect(httpResponse.status).toBe(404);
expect(httpResponse.buffer).toEqual(new Buffer('NO'));
expect(httpResponse.text).toEqual('NO');
expect(httpResponse.data).toBe(undefined);
done();
})
})
it("should post on echo", (done) => {
var calls = 0;
httpRequest({
method: "POST",
url: httpRequestServer+"/echo",
body: {
foo: "bar"
},
headers: {
'Content-Type': 'application/json'
},
success: function() { calls++; },
error: function() { calls++; }
}).then(function(httpResponse){
expect(calls).toBe(1);
expect(httpResponse.status).toBe(200);
expect(httpResponse.data).toEqual({foo: "bar"});
done();
}, function(httpResponse){
fail("should not fail");
done();
})
});
it("should encode a query string body by default", (done) => {
let options = {
body: {"foo": "bar"},
}
let result = httpRequest.encodeBody(options);
expect(result.body).toEqual('foo=bar');
expect(result.headers['Content-Type']).toEqual('application/x-www-form-urlencoded');
done();
})
it("should encode a JSON body", (done) => {
let options = {
body: {"foo": "bar"},
headers: {'Content-Type': 'application/json'}
}
let result = httpRequest.encodeBody(options);
expect(result.body).toEqual('{"foo":"bar"}');
done();
})
it("should encode a www-form body", (done) => {
let options = {
body: {"foo": "bar", "bar": "baz"},
headers: {'cOntent-tYpe': 'application/x-www-form-urlencoded'}
}
let result = httpRequest.encodeBody(options);
expect(result.body).toEqual("foo=bar&bar=baz");
done();
});
it("should not encode a wrong content type", (done) => {
let options = {
body:{"foo": "bar", "bar": "baz"},
headers: {'cOntent-tYpe': 'mime/jpeg'}
}
let result = httpRequest.encodeBody(options);
expect(result.body).toEqual({"foo": "bar", "bar": "baz"});
done();
});
it("should fail gracefully", (done) => {
httpRequest({
url: "http://not a good url",
success: function() {
fail("should not succeed");
done();
},
error: function(error) {
expect(error).not.toBeUndefined();
expect(error).not.toBeNull();
done();
}
});
});
it('should get a cat image', (done) => {
httpRequest({
url: 'http://thecatapi.com/api/images/get?format=src&type=jpg',
followRedirects: true
}).then((res) => {
expect(res.buffer).not.toBe(null);
expect(res.text).not.toBe(null);
done();
})
})
it("should params object to query string", (done) => {
httpRequest({
url: httpRequestServer+"/qs",
params: {
foo: "bar"
}
}).then(function(httpResponse){
expect(httpResponse.status).toBe(200);
expect(httpResponse.data).toEqual({foo: "bar"});
done();
}, function(){
fail("should not fail");
done();
})
});
it("should params string to query string", (done) => {
httpRequest({
url: httpRequestServer+"/qs",
params: "foo=bar&foo2=bar2"
}).then(function(httpResponse){
expect(httpResponse.status).toBe(200);
expect(httpResponse.data).toEqual({foo: "bar", foo2: 'bar2'});
done();
}, function(){
fail("should not fail");
done();
})
});
});
+62 -25
View File
@@ -2,53 +2,90 @@ var LoggerController = require('../src/Controllers/LoggerController').LoggerCont
var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter;
describe('LoggerController', () => {
it('can check valid master key of request', (done) => {
it('can check process a query witout throwing', (done) => {
// Make mock request
var request = {
auth: {
isMaster: true
},
query: {}
};
var query = {};
var loggerController = new LoggerController(new FileLoggerAdapter());
expect(() => {
loggerController.handleGET(request);
loggerController.getLogs(query).then(function(res) {
expect(res.length).not.toBe(0);
done();
}).catch((err) => {
console.error(err);
fail("should not fail");
done();
})
}).not.toThrow();
});
it('properly validates dateTimes', (done) => {
expect(LoggerController.validDateTime()).toBe(null);
expect(LoggerController.validDateTime("String")).toBe(null);
expect(LoggerController.validDateTime(123456).getTime()).toBe(123456);
expect(LoggerController.validDateTime("2016-01-01Z00:00:00").getTime()).toBe(1451606400000);
done();
});
it('can check invalid construction of controller', (done) => {
it('can set the proper default values', (done) => {
// Make mock request
var request = {
auth: {
isMaster: true
},
query: {}
var result = LoggerController.parseOptions();
expect(result.size).toEqual(10);
expect(result.order).toEqual('desc');
expect(result.level).toEqual('info');
done();
});
it('can process a query witout throwing', (done) => {
// Make mock request
var query = {
from: "2016-01-01Z00:00:00",
until: "2016-01-01Z00:00:00",
size: 5,
order: 'asc',
level: 'error'
};
var loggerController = new LoggerController();
var result = LoggerController.parseOptions(query);
expect(result.from.getTime()).toEqual(1451606400000);
expect(result.until.getTime()).toEqual(1451606400000);
expect(result.size).toEqual(5);
expect(result.order).toEqual('asc');
expect(result.level).toEqual('error');
expect(() => {
loggerController.handleGET(request);
}).toThrow();
done();
});
it('can check invalid master key of request', (done) => {
it('can check process a query witout throwing', (done) => {
// Make mock request
var request = {
auth: {
isMaster: false
},
query: {}
var query = {
from: "2016-01-01",
until: "2016-01-30",
size: 5,
order: 'desc',
level: 'error'
};
var loggerController = new LoggerController(new FileLoggerAdapter());
expect(() => {
loggerController.handleGET(request);
loggerController.getLogs(query).then(function(res) {
expect(res.length).toBe(0);
done();
}).catch((err) => {
console.error(err);
fail("should not fail");
done();
})
}).not.toThrow();
});
it('should throw without an adapter', (done) => {
expect(() => {
var loggerController = new LoggerController();
}).toThrow();
done();
});
+65
View File
@@ -0,0 +1,65 @@
'use strict';
const request = require('request');
var LogsRouter = require('../src/Routers/LogsRouter').LogsRouter;
var LoggerController = require('../src/Controllers/LoggerController').LoggerController;
var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter;
const loggerController = new LoggerController(new FileLoggerAdapter());
describe('LogsRouter', () => {
it('can check valid master key of request', (done) => {
// Make mock request
var request = {
auth: {
isMaster: true
},
query: {},
config: {
loggerController: loggerController
}
};
var router = new LogsRouter();
expect(() => {
router.validateRequest(request);
}).not.toThrow();
done();
});
it('can check invalid construction of controller', (done) => {
// Make mock request
var request = {
auth: {
isMaster: true
},
query: {},
config: {
loggerController: undefined // missing controller
}
};
var router = new LogsRouter();
expect(() => {
router.validateRequest(request);
}).toThrow();
done();
});
it('can check invalid master key of request', done => {
request.get({
url: 'http://localhost:8378/1/scriptlog',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(403);
expect(body.error).toEqual('unauthorized: master key is required');
done();
});
});
});
+5
View File
@@ -0,0 +1,5 @@
module.exports = function(options) {
return {
options: options
};
};
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve()
}
+10
View File
@@ -0,0 +1,10 @@
module.exports = options => {
if (!options) {
throw "Options were not provided"
}
return {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve()
}
}
+37
View File
@@ -0,0 +1,37 @@
'use strict';
const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
const MongoClient = require('mongodb').MongoClient;
describe('MongoStorageAdapter', () => {
it('auto-escapes symbols in auth information', () => {
spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(null));
new MongoStorageAdapter('mongodb://user!with@+ symbols:password!with@+ symbols@localhost:1234/parse', {})
.connect();
expect(MongoClient.connect).toHaveBeenCalledWith(
'mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse',
jasmine.any(Object)
);
});
it("doesn't double escape already URI-encoded information", () => {
spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(null));
new MongoStorageAdapter('mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse', {})
.connect();
expect(MongoClient.connect).toHaveBeenCalledWith(
'mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse',
jasmine.any(Object)
);
});
// https://github.com/ParsePlatform/parse-server/pull/148#issuecomment-180407057
it('preserves replica sets', () => {
spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(null));
new MongoStorageAdapter('mongodb://test:testpass@ds056315-a0.mongolab.com:59325,ds059315-a1.mongolab.com:59315/testDBname?replicaSet=rs-ds059415', {})
.connect();
expect(MongoClient.connect).toHaveBeenCalledWith(
'mongodb://test:testpass@ds056315-a0.mongolab.com:59325,ds059315-a1.mongolab.com:59315/testDBname?replicaSet=rs-ds059415',
jasmine.any(Object)
);
});
});
+50 -31
View File
@@ -1,5 +1,6 @@
var OAuth = require("../src/oauth/OAuth1Client");
var OAuth = require("../src/authDataManager/OAuth1Client");
var request = require('request');
var Config = require("../src/Config");
describe('OAuth', function() {
@@ -28,7 +29,7 @@ describe('OAuth', function() {
it("Should properly generate request signature", (done) => {
var request = {
host: "dummy.com",
host: "dummy.com",
path: "path"
};
@@ -48,7 +49,7 @@ describe('OAuth', function() {
it("Should properly build request", (done) => {
var options = {
host: "dummy.com",
host: "dummy.com",
consumer_key: "hello",
consumer_secret: "world",
auth_token: "token",
@@ -86,7 +87,7 @@ describe('OAuth', function() {
it("Should fail a GET request", (done) => {
var options = {
host: "api.twitter.com",
host: "api.twitter.com",
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
};
@@ -100,7 +101,7 @@ describe('OAuth', function() {
it("Should fail a POST request", (done) => {
var options = {
host: "api.twitter.com",
host: "api.twitter.com",
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
};
@@ -117,7 +118,7 @@ describe('OAuth', function() {
it("Should fail a request", (done) => {
var options = {
host: "localhost",
host: "localhost",
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
};
@@ -138,11 +139,11 @@ describe('OAuth', function() {
["facebook", "github", "instagram", "google", "linkedin", "meetup", "twitter"].map(function(providerName){
it("Should validate structure of "+providerName, (done) => {
var provider = require("../src/oauth/"+providerName);
var provider = require("../src/authDataManager/"+providerName);
jequal(typeof provider.validateAuthData, "function");
jequal(typeof provider.validateAppId, "function");
jequal(provider.validateAuthData({}, {}).constructor, Promise.prototype.constructor);
jequal(provider.validateAppId("app", "key", {}).constructor, Promise.prototype.constructor);
jequal(provider.validateAuthData({}, {}).constructor, Promise.prototype.constructor);
jequal(provider.validateAppId("app", "key", {}).constructor, Promise.prototype.constructor);
done();
});
});
@@ -203,13 +204,11 @@ describe('OAuth', function() {
myoauth: getMockMyOauthProvider().authData
}
};
var headers = {'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'Content-Type': 'application/json' }
var options = {
headers: {'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Installation-Id': 'yolo',
'Content-Type': 'application/json' },
url: 'http://localhost:8378/1/users',
body: JSON.stringify(jsonBody)
@@ -223,9 +222,19 @@ describe('OAuth', function() {
createOAuthUser((error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
ok(b.sessionToken);
expect(b.objectId).not.toBeNull();
expect(b.objectId).not.toBeUndefined();
done();
var sessionToken = b.sessionToken;
var q = new Parse.Query("_Session");
q.equalTo('sessionToken', sessionToken);
q.first({useMasterKey: true}).then((res) => {
expect(res.get("installationId")).toEqual('yolo');
done();
}).fail((err) => {
fail('should not fail fetching the session');
done();
})
});
});
@@ -248,7 +257,7 @@ describe('OAuth', function() {
done();
});
});
});
it("unlink and link with custom provider", (done) => {
@@ -266,28 +275,38 @@ describe('OAuth', function() {
model._unlinkFrom("myoauth", {
success: function(model) {
ok(!model._isLinked("myoauth"),
"User should not be linked to myoauth");
ok(!provider.synchronizedUserId, "User id should be cleared");
ok(!provider.synchronizedAuthToken, "Auth token should be cleared");
ok(!provider.synchronizedExpiration,
"Expiration should be cleared");
// make sure the auth data is properly deleted
var config = new Config(Parse.applicationId);
config.database.mongoFind('_User', {
_id: model.id
}).then((res) => {
expect(res.length).toBe(1);
expect(res[0]._auth_data_myoauth).toBeUndefined();
expect(res[0]._auth_data_myoauth).not.toBeNull();
model._linkWith("myoauth", {
success: function(model) {
ok(provider.synchronizedUserId, "User id should have a value");
ok(provider.synchronizedAuthToken,
"Auth token should have a value");
ok(provider.synchronizedExpiration,
"Expiration should have a value");
ok(model._isLinked("myoauth"),
"User should be linked to myoauth");
done();
},
error: function(model, error) {
ok(false, "linking again should succeed");
done();
}
model._linkWith("myoauth", {
success: function(model) {
ok(provider.synchronizedUserId, "User id should have a value");
ok(provider.synchronizedAuthToken,
"Auth token should have a value");
ok(provider.synchronizedExpiration,
"Expiration should have a value");
ok(model._isLinked("myoauth"),
"User should be linked to myoauth");
done();
},
error: function(model, error) {
ok(false, "linking again should succeed");
done();
}
});
});
},
error: function(model, error) {
@@ -304,4 +323,4 @@ describe('OAuth', function() {
});
})
})
-234
View File
@@ -1,234 +0,0 @@
var OneSignalPushAdapter = require('../src/Adapters/Push/OneSignalPushAdapter');
describe('OneSignalPushAdapter', () => {
it('can be initialized', (done) => {
// Make mock config
var pushConfig = {
oneSignalAppId:"APP ID",
oneSignalApiKey:"API KEY"
};
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
var senderMap = oneSignalPushAdapter.senderMap;
expect(senderMap.ios instanceof Function).toBe(true);
expect(senderMap.android instanceof Function).toBe(true);
done();
});
it('can get valid push types', (done) => {
var oneSignalPushAdapter = new OneSignalPushAdapter();
expect(oneSignalPushAdapter.getValidPushTypes()).toEqual(['ios', 'android']);
done();
});
it('can classify installation', (done) => {
// Mock installations
var validPushTypes = ['ios', 'android'];
var installations = [
{
deviceType: 'android',
deviceToken: 'androidToken'
},
{
deviceType: 'ios',
deviceToken: 'iosToken'
},
{
deviceType: 'win',
deviceToken: 'winToken'
},
{
deviceType: 'android',
deviceToken: undefined
}
];
var deviceMap = OneSignalPushAdapter.classifyInstallation(installations, validPushTypes);
expect(deviceMap['android']).toEqual([makeDevice('androidToken')]);
expect(deviceMap['ios']).toEqual([makeDevice('iosToken')]);
expect(deviceMap['win']).toBe(undefined);
done();
});
it('can send push notifications', (done) => {
var oneSignalPushAdapter = new OneSignalPushAdapter();
// Mock android ios senders
var androidSender = jasmine.createSpy('send')
var iosSender = jasmine.createSpy('send')
var senderMap = {
ios: iosSender,
android: androidSender
};
oneSignalPushAdapter.senderMap = senderMap;
// Mock installations
var installations = [
{
deviceType: 'android',
deviceToken: 'androidToken'
},
{
deviceType: 'ios',
deviceToken: 'iosToken'
},
{
deviceType: 'win',
deviceToken: 'winToken'
},
{
deviceType: 'android',
deviceToken: undefined
}
];
var data = {};
oneSignalPushAdapter.send(data, installations);
// Check android sender
expect(androidSender).toHaveBeenCalled();
var args = androidSender.calls.first().args;
expect(args[0]).toEqual(data);
expect(args[1]).toEqual([
makeDevice('androidToken')
]);
// Check ios sender
expect(iosSender).toHaveBeenCalled();
args = iosSender.calls.first().args;
expect(args[0]).toEqual(data);
expect(args[1]).toEqual([
makeDevice('iosToken')
]);
done();
});
it("can send iOS notifications", (done) => {
var oneSignalPushAdapter = new OneSignalPushAdapter();
var sendToOneSignal = jasmine.createSpy('sendToOneSignal');
oneSignalPushAdapter.sendToOneSignal = sendToOneSignal;
oneSignalPushAdapter.sendToAPNS({'data':{
'badge': 1,
'alert': "Example content",
'sound': "Example sound",
'content-available': 1,
'misc-data': 'Example Data'
}},[{'deviceToken':'iosToken1'},{'deviceToken':'iosToken2'}])
expect(sendToOneSignal).toHaveBeenCalled();
var args = sendToOneSignal.calls.first().args;
expect(args[0]).toEqual({
'ios_badgeType':'SetTo',
'ios_badgeCount':1,
'contents': { 'en':'Example content'},
'ios_sound': 'Example sound',
'content_available':true,
'data':{'misc-data':'Example Data'},
'include_ios_tokens':['iosToken1','iosToken2']
})
done();
});
it("can send Android notifications", (done) => {
var oneSignalPushAdapter = new OneSignalPushAdapter();
var sendToOneSignal = jasmine.createSpy('sendToOneSignal');
oneSignalPushAdapter.sendToOneSignal = sendToOneSignal;
oneSignalPushAdapter.sendToGCM({'data':{
'title': 'Example title',
'alert': 'Example content',
'misc-data': 'Example Data'
}},[{'deviceToken':'androidToken1'},{'deviceToken':'androidToken2'}])
expect(sendToOneSignal).toHaveBeenCalled();
var args = sendToOneSignal.calls.first().args;
expect(args[0]).toEqual({
'contents': { 'en':'Example content'},
'title': {'en':'Example title'},
'data':{'misc-data':'Example Data'},
'include_android_reg_ids': ['androidToken1','androidToken2']
})
done();
});
it("can post the correct data", (done) => {
var pushConfig = {
oneSignalAppId:"APP ID",
oneSignalApiKey:"API KEY"
};
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
var write = jasmine.createSpy('write');
oneSignalPushAdapter.https = {
'request': function(a,b) {
return {
'end':function(){},
'on':function(a,b){},
'write':write
}
}
};
var installations = [
{
deviceType: 'android',
deviceToken: 'androidToken'
},
{
deviceType: 'ios',
deviceToken: 'iosToken'
},
{
deviceType: 'win',
deviceToken: 'winToken'
},
{
deviceType: 'android',
deviceToken: undefined
}
];
oneSignalPushAdapter.send({'data':{
'title': 'Example title',
'alert': 'Example content',
'content-available':1,
'misc-data': 'Example Data'
}}, installations);
expect(write).toHaveBeenCalled();
// iOS
args = write.calls.first().args;
expect(args[0]).toEqual(JSON.stringify({
'contents': { 'en':'Example content'},
'content_available':true,
'data':{'title':'Example title','misc-data':'Example Data'},
'include_ios_tokens':['iosToken'],
'app_id':'APP ID'
}));
// Android
args = write.calls.mostRecent().args;
expect(args[0]).toEqual(JSON.stringify({
'contents': { 'en':'Example content'},
'title': {'en':'Example title'},
'data':{"content-available":1,'misc-data':'Example Data'},
'include_android_reg_ids':['androidToken'],
'app_id':'APP ID'
}));
done();
});
function makeDevice(deviceToken, appIdentifier) {
return {
deviceToken: deviceToken
};
}
});
+92
View File
@@ -0,0 +1,92 @@
'use strict';
describe('Parse.Push', () => {
var setup = function() {
var pushAdapter = {
send: function(body, installations) {
var badge = body.data.badge;
let promises = installations.map((installation) => {
if (installation.deviceType == "ios") {
expect(installation.badge).toEqual(badge);
expect(installation.originalBadge+1).toEqual(installation.badge);
} else {
expect(installation.badge).toBeUndefined();
}
return Promise.resolve({
err: null,
deviceType: installation.deviceType,
result: true
})
});
return Promise.all(promises);
},
getValidPushTypes: function() {
return ["ios", "android"];
}
}
setServerConfiguration({
appId: Parse.applicationId,
masterKey: Parse.masterKey,
serverURL: Parse.serverURL,
push: {
adapter: pushAdapter
}
});
var installations = [];
while(installations.length != 10) {
var installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_"+installations.length);
installation.set("deviceToken","device_token_"+installations.length)
installation.set("badge", installations.length);
installation.set("originalBadge", installations.length);
installation.set("deviceType", "ios");
installations.push(installation);
}
return Parse.Object.saveAll(installations);
}
it('should properly send push', (done) => {
return setup().then(() => {
return Parse.Push.send({
where: {
deviceType: 'ios'
},
data: {
badge: 'Increment',
alert: 'Hello world!'
}
}, {useMasterKey: true})
})
.then(() => {
done();
}, (err) => {
console.error();
fail('should not fail sending push')
done();
});
});
it('should properly send push with lowercaseIncrement', (done) => {
return setup().then(() => {
return Parse.Push.send({
where: {
deviceType: 'ios'
},
data: {
badge: 'increment',
alert: 'Hello world!'
}
}, {useMasterKey: true})
}).then(() => {
done();
}, (err) => {
console.error();
fail('should not fail sending push')
done();
});
});
});
+52 -1
View File
@@ -1,5 +1,9 @@
// This is a port of the test suite:
// hungry/js/test/parse_acl_test.js
var rest = require('../src/rest');
var Config = require('../src/Config');
var config = new Config('test');
var auth = require('../src/Auth');
describe('Parse.ACL', () => {
it("acl must be valid", (done) => {
@@ -786,7 +790,11 @@ describe('Parse.ACL', () => {
equal(results.length, 1);
var result = results[0];
ok(result);
equal(result.id, object.id);
if (!result) {
fail("should have result");
} else {
equal(result.id, object.id);
}
done();
}
});
@@ -1141,4 +1149,47 @@ describe('Parse.ACL', () => {
}));
});
it('restricted ACL does not have public access', (done) => {
var obj = new Parse.Object("TestClassMasterACL");
var acl = new Parse.ACL();
obj.set('ACL', acl);
obj.save().then(() => {
var query = new Parse.Query("TestClassMasterACL");
return query.find();
}).then((results) => {
ok(!results.length, 'Should not have returned object with secure ACL.');
done();
});
});
it('regression test #701', done => {
var anonUser = {
authData: {
anonymous: {
id: '00000000-0000-0000-0000-000000000001'
}
}
};
Parse.Cloud.afterSave(Parse.User, req => {
if (!req.object.existed()) {
var user = req.object;
var acl = new Parse.ACL(user);
user.setACL(acl);
user.save(null, {useMasterKey: true}).then(user => {
new Parse.Query('_User').get(user.objectId).then(user => {
fail('should not have fetched user without public read enabled');
done();
}, error => {
expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
Parse.Cloud._removeHook('Triggers', 'afterSave', Parse.User.className);
done();
});
});
}
});
rest.create(config, auth.nobody(config), '_User', anonUser)
})
});
+653 -103
View File
@@ -1,8 +1,10 @@
// A bunch of different tests are in here - it isn't very thematic.
// It would probably be better to refactor them into different files.
'use strict';
var DatabaseAdapter = require('../src/DatabaseAdapter');
var request = require('request');
const Parse = require("parse/node");
describe('miscellaneous', function() {
it('create a GameScore object', function(done) {
@@ -138,7 +140,7 @@ describe('miscellaneous', function() {
return new Parse.Query(TestObject).find();
}).then((results) => {
expect(results.length).toEqual(100);
done();
done();
}, (error) => {
fail(error);
done();
@@ -222,6 +224,19 @@ describe('miscellaneous', function() {
});
});
it('test beforeSave returns value on create and update', (done) => {
var obj = new Parse.Object('BeforeSaveChanged');
obj.set('foo', 'bing');
obj.save().then(() => {
expect(obj.get('foo')).toEqual('baz');
obj.set('foo', 'bar');
return obj.save().then(() => {
expect(obj.get('foo')).toEqual('baz');
done();
})
})
});
it('test afterSave ran and created an object', function(done) {
var obj = new Parse.Object('AfterSaveTest');
obj.save();
@@ -274,13 +289,17 @@ describe('miscellaneous', function() {
var objAgain = new Parse.Object('BeforeDeleteFail', {objectId: id});
return objAgain.fetch();
}).then((objAgain) => {
expect(objAgain.get('foo')).toEqual('bar');
if (objAgain) {
expect(objAgain.get('foo')).toEqual('bar');
} else {
fail("unable to fetch the object ", id);
}
done();
}, (error) => {
// We should have been able to fetch the object again
fail(error);
});
})
});
it('basic beforeDelete rejection via promise', function(done) {
var obj = new Parse.Object('BeforeDeleteFailWithPromise');
@@ -350,6 +369,11 @@ describe('miscellaneous', function() {
it('test cloud function return types', function(done) {
Parse.Cloud.run('foo').then((result) => {
expect(result.object instanceof Parse.Object).toBeTruthy();
if (!result.object) {
fail("Unable to run foo");
done();
return;
}
expect(result.object.className).toEqual('Foo');
expect(result.object.get('x')).toEqual(2);
var bar = result.object.get('relation');
@@ -363,6 +387,22 @@ describe('miscellaneous', function() {
});
});
it('test cloud function should echo keys', function(done) {
Parse.Cloud.run('echoKeys').then((result) => {
expect(result.applicationId).toEqual(Parse.applicationId);
expect(result.masterKey).toEqual(Parse.masterKey);
expect(result.javascriptKey).toEqual(Parse.javascriptKey);
done();
});
});
it('should properly create an object in before save', (done) => {
Parse.Cloud.run('createBeforeSaveChangedObject').then((res) => {
expect(res.get('foo')).toEqual('baz');
done();
});
})
it('test rest_create_app', function(done) {
var appId;
Parse._request('POST', 'rest_create_app').then((res) => {
@@ -374,55 +414,239 @@ describe('miscellaneous', function() {
obj.set('foo', 'bar');
return obj.save();
}).then(() => {
var db = DatabaseAdapter.getDatabaseConnection(appId);
var db = DatabaseAdapter.getDatabaseConnection(appId, 'test_');
return db.mongoFind('TestObject', {}, {});
}).then((results) => {
expect(results.length).toEqual(1);
expect(results[0]['foo']).toEqual('bar');
done();
}).fail(err => {
fail(err);
done();
})
});
describe('beforeSave', () => {
beforeEach(done => {
// Make sure the required mock for all tests is unset.
Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore");
done();
});
afterEach(done => {
// Make sure the required mock for all tests is unset.
Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore");
done();
});
it('object is set on create and update', done => {
let triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.beforeSave('GameScore', (req, res) => {
let object = req.object;
expect(object instanceof Parse.Object).toBeTruthy();
expect(object.get('fooAgain')).toEqual('barAgain');
if (triggerTime == 0) {
// Create
expect(object.get('foo')).toEqual('bar');
// No objectId/createdAt/updatedAt
expect(object.id).toBeUndefined();
expect(object.createdAt).toBeUndefined();
expect(object.updatedAt).toBeUndefined();
} else if (triggerTime == 1) {
// Update
expect(object.get('foo')).toEqual('baz');
expect(object.id).not.toBeUndefined();
expect(object.createdAt).not.toBeUndefined();
expect(object.updatedAt).not.toBeUndefined();
} else {
res.error();
}
triggerTime++;
res.success();
});
let obj = new Parse.Object('GameScore');
obj.set('foo', 'bar');
obj.set('fooAgain', 'barAgain');
obj.save().then(() => {
// We only update foo
obj.set('foo', 'baz');
return obj.save();
}).then(() => {
// Make sure the checking has been triggered
expect(triggerTime).toBe(2);
done();
}, error => {
fail(error);
done();
});
});
it('dirtyKeys are set on update', done => {
let triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.beforeSave('GameScore', (req, res) => {
var object = req.object;
expect(object instanceof Parse.Object).toBeTruthy();
expect(object.get('fooAgain')).toEqual('barAgain');
if (triggerTime == 0) {
// Create
expect(object.get('foo')).toEqual('bar');
} else if (triggerTime == 1) {
// Update
expect(object.dirtyKeys()).toEqual(['foo']);
expect(object.dirty('foo')).toBeTruthy();
expect(object.get('foo')).toEqual('baz');
} else {
res.error();
}
triggerTime++;
res.success();
});
let obj = new Parse.Object('GameScore');
obj.set('foo', 'bar');
obj.set('fooAgain', 'barAgain');
obj.save().then(() => {
// We only update foo
obj.set('foo', 'baz');
return obj.save();
}).then(() => {
// Make sure the checking has been triggered
expect(triggerTime).toBe(2);
done();
}, function(error) {
fail(error);
done();
});
});
it('original object is set on update', done => {
let triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.beforeSave('GameScore', (req, res) => {
let object = req.object;
expect(object instanceof Parse.Object).toBeTruthy();
expect(object.get('fooAgain')).toEqual('barAgain');
let originalObject = req.original;
if (triggerTime == 0) {
// No id/createdAt/updatedAt
expect(object.id).toBeUndefined();
expect(object.createdAt).toBeUndefined();
expect(object.updatedAt).toBeUndefined();
// Create
expect(object.get('foo')).toEqual('bar');
// Check the originalObject is undefined
expect(originalObject).toBeUndefined();
} else if (triggerTime == 1) {
// Update
expect(object.id).not.toBeUndefined();
expect(object.createdAt).not.toBeUndefined();
expect(object.updatedAt).not.toBeUndefined();
expect(object.get('foo')).toEqual('baz');
// Check the originalObject
expect(originalObject instanceof Parse.Object).toBeTruthy();
expect(originalObject.get('fooAgain')).toEqual('barAgain');
expect(originalObject.id).not.toBeUndefined();
expect(originalObject.createdAt).not.toBeUndefined();
expect(originalObject.updatedAt).not.toBeUndefined();
expect(originalObject.get('foo')).toEqual('bar');
} else {
res.error();
}
triggerTime++;
res.success();
});
let obj = new Parse.Object('GameScore');
obj.set('foo', 'bar');
obj.set('fooAgain', 'barAgain');
obj.save().then(() => {
// We only update foo
obj.set('foo', 'baz');
return obj.save();
}).then(() => {
// Make sure the checking has been triggered
expect(triggerTime).toBe(2);
done();
}, error => {
fail(error);
done();
});
});
it('pointer mutation properly saves object', done => {
let className = 'GameScore';
Parse.Cloud.beforeSave(className, (req, res) => {
let object = req.object;
expect(object instanceof Parse.Object).toBeTruthy();
let child = object.get('child');
expect(child instanceof Parse.Object).toBeTruthy();
child.set('a', 'b');
child.save().then(() => {
res.success();
});
});
let obj = new Parse.Object(className);
obj.set('foo', 'bar');
let child = new Parse.Object('Child');
child.save().then(() => {
obj.set('child', child);
return obj.save();
}).then(() => {
let query = new Parse.Query(className);
query.include('child');
return query.get(obj.id).then(objAgain => {
expect(objAgain.get('foo')).toEqual('bar');
let childAgain = objAgain.get('child');
expect(childAgain instanceof Parse.Object).toBeTruthy();
expect(childAgain.get('a')).toEqual('b');
return Promise.resolve();
});
}).then(() => {
done();
}, error => {
fail(error);
done();
});
});
});
it('test beforeSave get full object on create and update', function(done) {
var triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.beforeSave('GameScore', function(req, res) {
var object = req.object;
expect(object instanceof Parse.Object).toBeTruthy();
expect(object.get('fooAgain')).toEqual('barAgain');
expect(object.id).not.toBeUndefined();
expect(object.createdAt).not.toBeUndefined();
expect(object.updatedAt).not.toBeUndefined();
if (triggerTime == 0) {
// Create
expect(object.get('foo')).toEqual('bar');
} else if (triggerTime == 1) {
// Update
expect(object.get('foo')).toEqual('baz');
} else {
res.error();
}
triggerTime++;
res.success();
});
it('pointer reassign is working properly (#1288)', (done) => {
Parse.Cloud.beforeSave('GameScore', (req, res) => {
var obj = req.object;
if (obj.get('point')) {
return res.success();
}
var TestObject1 = Parse.Object.extend('TestObject1');
var newObj = new TestObject1({'key1': 1});
return newObj.save().then((newObj) => {
obj.set('point' , newObj);
res.success();
});
});
var pointId;
var obj = new Parse.Object('GameScore');
obj.set('foo', 'bar');
obj.set('fooAgain', 'barAgain');
obj.save().then(function() {
// We only update foo
obj.save().then(() => {
expect(obj.get('point')).not.toBeUndefined();
pointId = obj.get('point').id;
expect(pointId).not.toBeUndefined();
obj.set('foo', 'baz');
return obj.save();
}).then(function() {
// Make sure the checking has been triggered
expect(triggerTime).toBe(2);
// Clear mock beforeSave
delete Parse.Cloud.Triggers.beforeSave.GameScore;
}).then((obj) => {
expect(obj.get('point').id).toEqual(pointId);
Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore");
done();
}, function(error) {
fail(error);
done();
});
})
});
it('test afterSave get full object on create and update', function(done) {
@@ -431,10 +655,10 @@ describe('miscellaneous', function() {
Parse.Cloud.afterSave('GameScore', function(req, res) {
var object = req.object;
expect(object instanceof Parse.Object).toBeTruthy();
expect(object.get('fooAgain')).toEqual('barAgain');
expect(object.id).not.toBeUndefined();
expect(object.createdAt).not.toBeUndefined();
expect(object.updatedAt).not.toBeUndefined();
expect(object.get('fooAgain')).toEqual('barAgain');
if (triggerTime == 0) {
// Create
expect(object.get('foo')).toEqual('bar');
@@ -448,58 +672,6 @@ describe('miscellaneous', function() {
res.success();
});
var obj = new Parse.Object('GameScore');
obj.set('foo', 'bar');
obj.set('fooAgain', 'barAgain');
obj.save().then(function() {
// We only update foo
obj.set('foo', 'baz');
return obj.save();
}).then(function() {
// Make sure the checking has been triggered
expect(triggerTime).toBe(2);
// Clear mock afterSave
delete Parse.Cloud.Triggers.afterSave.GameScore;
done();
}, function(error) {
fail(error);
done();
});
});
it('test beforeSave get original object on update', function(done) {
var triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.beforeSave('GameScore', function(req, res) {
var object = req.object;
expect(object instanceof Parse.Object).toBeTruthy();
expect(object.get('fooAgain')).toEqual('barAgain');
expect(object.id).not.toBeUndefined();
expect(object.createdAt).not.toBeUndefined();
expect(object.updatedAt).not.toBeUndefined();
var originalObject = req.original;
if (triggerTime == 0) {
// Create
expect(object.get('foo')).toEqual('bar');
// Check the originalObject is undefined
expect(originalObject).toBeUndefined();
} else if (triggerTime == 1) {
// Update
expect(object.get('foo')).toEqual('baz');
// Check the originalObject
expect(originalObject instanceof Parse.Object).toBeTruthy();
expect(originalObject.get('fooAgain')).toEqual('barAgain');
expect(originalObject.id).not.toBeUndefined();
expect(originalObject.createdAt).not.toBeUndefined();
expect(originalObject.updatedAt).not.toBeUndefined();
expect(originalObject.get('foo')).toEqual('bar');
} else {
res.error();
}
triggerTime++;
res.success();
});
var obj = new Parse.Object('GameScore');
obj.set('foo', 'bar');
obj.set('fooAgain', 'barAgain');
@@ -511,7 +683,7 @@ describe('miscellaneous', function() {
// Make sure the checking has been triggered
expect(triggerTime).toBe(2);
// Clear mock beforeSave
delete Parse.Cloud.Triggers.beforeSave.GameScore;
Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore");
done();
}, function(error) {
fail(error);
@@ -522,6 +694,7 @@ describe('miscellaneous', function() {
it('test afterSave get original object on update', function(done) {
var triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.afterSave('GameScore', function(req, res) {
var object = req.object;
expect(object instanceof Parse.Object).toBeTruthy();
@@ -563,14 +736,233 @@ describe('miscellaneous', function() {
// Make sure the checking has been triggered
expect(triggerTime).toBe(2);
// Clear mock afterSave
delete Parse.Cloud.Triggers.afterSave.GameScore;
Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore");
done();
}, function(error) {
console.error(error);
fail(error);
done();
});
});
it('test afterSave get full original object even req auth can not query it', (done) => {
var triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.afterSave('GameScore', function(req, res) {
var object = req.object;
var originalObject = req.original;
if (triggerTime == 0) {
// Create
} else if (triggerTime == 1) {
// Update
expect(object.get('foo')).toEqual('baz');
// Make sure we get the full originalObject
expect(originalObject instanceof Parse.Object).toBeTruthy();
expect(originalObject.get('fooAgain')).toEqual('barAgain');
expect(originalObject.id).not.toBeUndefined();
expect(originalObject.createdAt).not.toBeUndefined();
expect(originalObject.updatedAt).not.toBeUndefined();
expect(originalObject.get('foo')).toEqual('bar');
} else {
res.error();
}
triggerTime++;
res.success();
});
var obj = new Parse.Object('GameScore');
obj.set('foo', 'bar');
obj.set('fooAgain', 'barAgain');
var acl = new Parse.ACL();
// Make sure our update request can not query the object
acl.setPublicReadAccess(false);
acl.setPublicWriteAccess(true);
obj.setACL(acl);
obj.save().then(function() {
// We only update foo
obj.set('foo', 'baz');
return obj.save();
}).then(function() {
// Make sure the checking has been triggered
expect(triggerTime).toBe(2);
// Clear mock afterSave
Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore");
done();
}, function(error) {
console.error(error);
fail(error);
done();
});
});
it('afterSave flattens custom operations', done => {
var triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.afterSave('GameScore', function(req, res) {
let object = req.object;
expect(object instanceof Parse.Object).toBeTruthy();
let originalObject = req.original;
if (triggerTime == 0) {
// Create
expect(object.get('yolo')).toEqual(1);
} else if (triggerTime == 1) {
// Update
expect(object.get('yolo')).toEqual(2);
// Check the originalObject
expect(originalObject.get('yolo')).toEqual(1);
} else {
res.error();
}
triggerTime++;
res.success();
});
var obj = new Parse.Object('GameScore');
obj.increment('yolo', 1);
obj.save().then(() => {
obj.increment('yolo', 1);
return obj.save();
}).then(() => {
// Make sure the checking has been triggered
expect(triggerTime).toBe(2);
// Clear mock afterSave
Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore");
done();
}, error => {
console.error(error);
fail(error);
done();
});
});
it('beforeSave receives ACL', done => {
let triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.beforeSave('GameScore', function(req, res) {
let object = req.object;
if (triggerTime == 0) {
let acl = object.getACL();
expect(acl.getPublicReadAccess()).toBeTruthy();
expect(acl.getPublicWriteAccess()).toBeTruthy();
} else if (triggerTime == 1) {
let acl = object.getACL();
expect(acl.getPublicReadAccess()).toBeFalsy();
expect(acl.getPublicWriteAccess()).toBeTruthy();
} else {
res.error();
}
triggerTime++;
res.success();
});
let obj = new Parse.Object('GameScore');
let acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setPublicWriteAccess(true);
obj.setACL(acl);
obj.save().then(() => {
acl.setPublicReadAccess(false);
obj.setACL(acl);
return obj.save();
}).then(() => {
// Make sure the checking has been triggered
expect(triggerTime).toBe(2);
// Clear mock afterSave
Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore");
done();
}, error => {
console.error(error);
fail(error);
done();
});
});
it('afterSave receives ACL', done => {
let triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.afterSave('GameScore', function(req, res) {
let object = req.object;
if (triggerTime == 0) {
let acl = object.getACL();
expect(acl.getPublicReadAccess()).toBeTruthy();
expect(acl.getPublicWriteAccess()).toBeTruthy();
} else if (triggerTime == 1) {
let acl = object.getACL();
expect(acl.getPublicReadAccess()).toBeFalsy();
expect(acl.getPublicWriteAccess()).toBeTruthy();
} else {
res.error();
}
triggerTime++;
res.success();
});
let obj = new Parse.Object('GameScore');
let acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setPublicWriteAccess(true);
obj.setACL(acl);
obj.save().then(() => {
acl.setPublicReadAccess(false);
obj.setACL(acl);
return obj.save();
}).then(() => {
// Make sure the checking has been triggered
expect(triggerTime).toBe(2);
// Clear mock afterSave
Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore");
done();
}, error => {
console.error(error);
fail(error);
done();
});
});
it('should return the updated fields on PUT', (done) => {
let obj = new Parse.Object('GameScore');
obj.save({a:'hello', c: 1, d: ['1'], e:['1'], f:['1','2']}).then(( ) => {
var headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Installation-Id': 'yolo'
};
request.put({
headers: headers,
url: 'http://localhost:8378/1/classes/GameScore/'+obj.id,
body: JSON.stringify({
a: 'b',
c: {"__op":"Increment","amount":2},
d: {"__op":"Add", objects: ['2']},
e: {"__op":"AddUnique", objects: ['1', '2']},
f: {"__op":"Remove", objects: ['2']},
selfThing: {"__type":"Pointer","className":"GameScore","objectId":obj.id},
})
}, (error, response, body) => {
body = JSON.parse(body);
expect(body.a).toBeUndefined();
expect(body.c).toEqual(3); // 2+1
expect(body.d.length).toBe(2);
expect(body.d.indexOf('1') > -1).toBe(true);
expect(body.d.indexOf('2') > -1).toBe(true);
expect(body.e.length).toBe(2);
expect(body.e.indexOf('1') > -1).toBe(true);
expect(body.e.indexOf('2') > -1).toBe(true);
expect(body.f.length).toBe(1);
expect(body.f.indexOf('1') > -1).toBe(true);
// return nothing on other self
expect(body.selfThing).toBeUndefined();
// updatedAt is always set
expect(body.updatedAt).not.toBeUndefined();
done();
});
}).fail((err) => {
fail('Should not fail');
done();
})
})
it('test cloud function error handling', (done) => {
// Register a function which will fail
Parse.Cloud.define('willFail', (req, res) => {
@@ -578,16 +970,90 @@ describe('miscellaneous', function() {
});
Parse.Cloud.run('willFail').then((s) => {
fail('Should not have succeeded.');
delete Parse.Cloud.Functions['willFail'];
Parse.Cloud._removeHook("Functions", "willFail");
done();
}, (e) => {
expect(e.code).toEqual(141);
expect(e.message).toEqual('noway');
delete Parse.Cloud.Functions['willFail'];
Parse.Cloud._removeHook("Functions", "willFail");
done();
});
});
it('test beforeSave/afterSave get installationId', function(done) {
let triggerTime = 0;
Parse.Cloud.beforeSave('GameScore', function(req, res) {
triggerTime++;
expect(triggerTime).toEqual(1);
expect(req.installationId).toEqual('yolo');
res.success();
});
Parse.Cloud.afterSave('GameScore', function(req) {
triggerTime++;
expect(triggerTime).toEqual(2);
expect(req.installationId).toEqual('yolo');
});
var headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Installation-Id': 'yolo'
};
request.post({
headers: headers,
url: 'http://localhost:8378/1/classes/GameScore',
body: JSON.stringify({ a: 'b' })
}, (error, response, body) => {
expect(error).toBe(null);
expect(triggerTime).toEqual(2);
Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore");
Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore");
done();
});
});
it('test beforeDelete/afterDelete get installationId', function(done) {
let triggerTime = 0;
Parse.Cloud.beforeDelete('GameScore', function(req, res) {
triggerTime++;
expect(triggerTime).toEqual(1);
expect(req.installationId).toEqual('yolo');
res.success();
});
Parse.Cloud.afterDelete('GameScore', function(req) {
triggerTime++;
expect(triggerTime).toEqual(2);
expect(req.installationId).toEqual('yolo');
});
var headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Installation-Id': 'yolo'
};
request.post({
headers: headers,
url: 'http://localhost:8378/1/classes/GameScore',
body: JSON.stringify({ a: 'b' })
}, (error, response, body) => {
expect(error).toBe(null);
request.del({
headers: headers,
url: 'http://localhost:8378/1/classes/GameScore/' + JSON.parse(body).objectId
}, (error, response, body) => {
expect(error).toBe(null);
expect(triggerTime).toEqual(2);
Parse.Cloud._removeHook("Triggers", "beforeDelete", "GameScore");
Parse.Cloud._removeHook("Triggers", "afterDelete", "GameScore");
done();
});
});
});
it('test cloud function query parameters', (done) => {
Parse.Cloud.define('echoParams', (req, res) => {
res.success(req.params);
@@ -612,7 +1078,7 @@ describe('miscellaneous', function() {
// Make sure query string params override body params
expect(res.other).toEqual('2');
expect(res.foo).toEqual("bar");
delete Parse.Cloud.Functions['echoParams'];
Parse.Cloud._removeHook("Functions",'echoParams');
done();
});
});
@@ -621,12 +1087,12 @@ describe('miscellaneous', function() {
// Register a function with validation
Parse.Cloud.define('functionWithParameterValidation', (req, res) => {
res.success('works');
}, (params) => {
return params.success === 100;
}, (request) => {
return request.params.success === 100;
});
Parse.Cloud.run('functionWithParameterValidation', {"success":100}).then((s) => {
delete Parse.Cloud.Functions['functionWithParameterValidation'];
Parse.Cloud._removeHook("Functions", "functionWithParameterValidation");
done();
}, (e) => {
fail('Validation should not have failed.');
@@ -638,13 +1104,13 @@ describe('miscellaneous', function() {
// Register a function with validation
Parse.Cloud.define('functionWithParameterValidationFailure', (req, res) => {
res.success('noway');
}, (params) => {
return params.success === 100;
}, (request) => {
return request.params.success === 100;
});
Parse.Cloud.run('functionWithParameterValidationFailure', {"success":500}).then((s) => {
fail('Validation should not have succeeded');
delete Parse.Cloud.Functions['functionWithParameterValidationFailure'];
Parse.Cloud._removeHook("Functions", "functionWithParameterValidationFailure");
done();
}, (e) => {
expect(e.code).toEqual(141);
@@ -721,4 +1187,88 @@ describe('miscellaneous', function() {
});
});
it('fails on invalid function', done => {
Parse.Cloud.run('somethingThatDoesDefinitelyNotExist').then((s) => {
fail('This should have never suceeded');
done();
}, (e) => {
expect(e.code).toEqual(Parse.Error.SCRIPT_FAILED);
expect(e.message).toEqual('Invalid function.');
done();
});
});
it('beforeSave change propagates through the save response', (done) => {
Parse.Cloud.beforeSave('ChangingObject', function(request, response) {
request.object.set('foo', 'baz');
response.success();
});
let obj = new Parse.Object('ChangingObject');
obj.save({ foo: 'bar' }).then((objAgain) => {
expect(objAgain.get('foo')).toEqual('baz');
Parse.Cloud._removeHook("Triggers", "beforeSave", "ChangingObject");
done();
}, (e) => {
Parse.Cloud._removeHook("Triggers", "beforeSave", "ChangingObject");
fail('Should not have failed to save.');
done();
});
});
it('dedupes an installation properly and returns updatedAt', (done) => {
let headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'
};
let data = {
'installationId': 'lkjsahdfkjhsdfkjhsdfkjhsdf',
'deviceType': 'embedded'
};
let requestOptions = {
headers: headers,
url: 'http://localhost:8378/1/installations',
body: JSON.stringify(data)
};
request.post(requestOptions, (error, response, body) => {
expect(error).toBe(null);
let b = JSON.parse(body);
expect(typeof b.objectId).toEqual('string');
request.post(requestOptions, (error, response, body) => {
expect(error).toBe(null);
let b = JSON.parse(body);
expect(typeof b.updatedAt).toEqual('string');
done();
});
});
});
it('android login providing empty authData block works', (done) => {
let headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'
};
let data = {
username: 'pulse1989',
password: 'password1234',
authData: {}
};
let requestOptions = {
headers: headers,
url: 'http://localhost:8378/1/users',
body: JSON.stringify(data)
};
request.post(requestOptions, (error, response, body) => {
expect(error).toBe(null);
requestOptions.url = 'http://localhost:8378/1/login';
request.get(requestOptions, (error, response, body) => {
expect(error).toBe(null);
let b = JSON.parse(body);
expect(typeof b['sessionToken']).toEqual('string');
done();
});
});
});
});
+70
View File
@@ -0,0 +1,70 @@
var ParseCloudCodePublisher = require('../src/LiveQuery/ParseCloudCodePublisher').ParseCloudCodePublisher;
var Parse = require('parse/node');
describe('ParseCloudCodePublisher', function() {
beforeEach(function(done) {
// Mock ParsePubSub
var mockParsePubSub = {
createPublisher: jasmine.createSpy('publish').and.returnValue({
publish: jasmine.createSpy('publish'),
on: jasmine.createSpy('on')
}),
createSubscriber: jasmine.createSpy('publish').and.returnValue({
subscribe: jasmine.createSpy('subscribe'),
on: jasmine.createSpy('on')
})
};
jasmine.mockLibrary('../src/LiveQuery/ParsePubSub', 'ParsePubSub', mockParsePubSub);
done();
});
it('can initialize', function() {
var config = {}
var publisher = new ParseCloudCodePublisher(config);
var ParsePubSub = require('../src/LiveQuery/ParsePubSub').ParsePubSub;
expect(ParsePubSub.createPublisher).toHaveBeenCalledWith(config);
});
it('can handle cloud code afterSave request', function() {
var publisher = new ParseCloudCodePublisher({});
publisher._onCloudCodeMessage = jasmine.createSpy('onCloudCodeMessage');
var request = {};
publisher.onCloudCodeAfterSave(request);
expect(publisher._onCloudCodeMessage).toHaveBeenCalledWith('afterSave', request);
});
it('can handle cloud code afterDelete request', function() {
var publisher = new ParseCloudCodePublisher({});
publisher._onCloudCodeMessage = jasmine.createSpy('onCloudCodeMessage');
var request = {};
publisher.onCloudCodeAfterDelete(request);
expect(publisher._onCloudCodeMessage).toHaveBeenCalledWith('afterDelete', request);
});
it('can handle cloud code request', function() {
var publisher = new ParseCloudCodePublisher({});
var currentParseObject = new Parse.Object('Test');
currentParseObject.set('key', 'value');
var originalParseObject = new Parse.Object('Test');
originalParseObject.set('key', 'originalValue');
var request = {
object: currentParseObject,
original: originalParseObject
};
publisher._onCloudCodeMessage('afterSave', request);
var args = publisher.parsePublisher.publish.calls.mostRecent().args;
expect(args[0]).toBe('afterSave');
var message = JSON.parse(args[1]);
expect(message.currentParseObject).toEqual(request.object._toFullJSON());
expect(message.originalParseObject).toEqual(request.original._toFullJSON());
});
afterEach(function(){
jasmine.restoreLibrary('../src/LiveQuery/ParsePubSub', 'ParsePubSub');
});
});
+60 -19
View File
@@ -1,6 +1,8 @@
// This is a port of the test suite:
// hungry/js/test/parse_file_test.js
"use strict";
var request = require('request');
var str = "Hello World!";
@@ -10,25 +12,49 @@ for (var i = 0; i < str.length; i++) {
}
describe('Parse.File testing', () => {
it('works with REST API', done => {
var headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'
};
request.post({
headers: headers,
url: 'http://localhost:8378/1/files/file.txt',
body: 'argle bargle',
}, (error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(b.name).toMatch(/_file.txt$/);
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.txt$/);
request.get(b.url, (error, response, body) => {
describe('creating files', () => {
it('works with Content-Type', done => {
var headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'
};
request.post({
headers: headers,
url: 'http://localhost:8378/1/files/file.txt',
body: 'argle bargle',
}, (error, response, body) => {
expect(error).toBe(null);
expect(body).toEqual('argle bargle');
done();
var b = JSON.parse(body);
expect(b.name).toMatch(/_file.txt$/);
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.txt$/);
request.get(b.url, (error, response, body) => {
expect(error).toBe(null);
expect(body).toEqual('argle bargle');
done();
});
});
});
it('works without Content-Type', done => {
var headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'
};
request.post({
headers: headers,
url: 'http://localhost:8378/1/files/file.txt',
body: 'argle bargle',
}, (error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(b.name).toMatch(/_file.txt$/);
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.txt$/);
request.get(b.url, (error, response, body) => {
expect(error).toBe(null);
expect(body).toEqual('argle bargle');
done();
});
});
});
});
@@ -458,7 +484,22 @@ describe('Parse.File testing', () => {
);
done();
});
});
it('supports files in objects without urls', done => {
var file = {
__type: 'File',
name: '123.txt'
};
var obj = new Parse.Object('FileTest');
obj.set('file', file);
obj.save().then(() => {
var query = new Parse.Query('FileTest');
return query.first();
}).then(result => {
let fileAgain = result.get('file');
expect(fileAgain.url()).toMatch(/123.txt$/);
done();
});
});
});
+34 -33
View File
@@ -1,25 +1,25 @@
'use strict';
var request = require('request');
var Parse = require('parse/node').Parse;
var DatabaseAdapter = require('../src/DatabaseAdapter');
var database = DatabaseAdapter.getDatabaseConnection('test');
let Config = require('../src/Config');
describe('a GlobalConfig', () => {
beforeEach(function(done) {
database.rawCollection('_GlobalConfig')
.then(coll => coll.updateOne({ '_id': 1}, { $set: { params: { companies: ['US', 'DK'] } } }, { upsert: true }))
.then(done());
beforeEach(done => {
let config = new Config('test');
config.database.adaptiveCollection('_GlobalConfig')
.then(coll => coll.upsertOne({ '_id': 1 }, { $set: { params: { companies: ['US', 'DK'] } } }))
.then(() => { done(); });
});
it('can be retrieved', (done) => {
request.get({
url: 'http://localhost:8378/1/config',
json: true,
url : 'http://localhost:8378/1/config',
json : true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
'X-Parse-Master-Key' : 'test'
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
expect(body.params.companies).toEqual(['US', 'DK']);
@@ -29,13 +29,13 @@ describe('a GlobalConfig', () => {
it('can be updated when a master key exists', (done) => {
request.put({
url: 'http://localhost:8378/1/config',
json: true,
body: { params: { companies: ['US', 'DK', 'SE'] } },
url : 'http://localhost:8378/1/config',
json : true,
body : { params: { companies: ['US', 'DK', 'SE'] } },
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test'
},
'X-Parse-Master-Key' : 'test'
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
expect(body.result).toEqual(true);
@@ -45,34 +45,35 @@ describe('a GlobalConfig', () => {
it('fail to update if master key is missing', (done) => {
request.put({
url: 'http://localhost:8378/1/config',
json: true,
body: { params: { companies: [] } },
url : 'http://localhost:8378/1/config',
json : true,
body : { params: { companies: [] } },
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'
},
'X-Parse-REST-API-Key' : 'rest'
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(401);
expect(body.error).toEqual('unauthorized');
expect(response.statusCode).toEqual(403);
expect(body.error).toEqual('unauthorized: master key is required');
done();
});
});
});
it('failed getting config when it is missing', (done) => {
database.rawCollection('_GlobalConfig')
.then(coll => coll.deleteOne({ '_id': 1}, {}, {}))
.then(_ => {
let config = new Config('test');
config.database.adaptiveCollection('_GlobalConfig')
.then(coll => coll.deleteOne({ '_id': 1 }))
.then(() => {
request.get({
url: 'http://localhost:8378/1/config',
json: true,
url : 'http://localhost:8378/1/config',
json : true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
'X-Parse-Master-Key' : 'test'
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(404);
expect(body.code).toEqual(Parse.Error.INVALID_KEY_NAME);
expect(response.statusCode).toEqual(200);
expect(body.params).toEqual({});
done();
});
});
+390
View File
@@ -0,0 +1,390 @@
/* global describe, it, expect, fail, Parse */
var request = require('request');
var triggers = require('../src/triggers');
var HooksController = require('../src/Controllers/HooksController').default;
var express = require("express");
var bodyParser = require('body-parser');
// Inject the hooks API
Parse.Hooks = require("../src/cloud-code/Parse.Hooks");
var port = 12345;
var hookServerURL = "http://localhost:"+port;
var app = express();
app.use(bodyParser.json({ 'type': '*/*' }))
app.listen(12345);
describe('Hooks', () => {
it("should have some hooks registered", (done) => {
Parse.Hooks.getFunctions().then((res) => {
expect(res.constructor).toBe(Array.prototype.constructor);
done();
}, (err) => {
fail(err);
done();
});
});
it("should have some triggers registered", (done) => {
Parse.Hooks.getTriggers().then( (res) => {
expect(res.constructor).toBe(Array.prototype.constructor);
done();
}, (err) => {
fail(err);
done();
});
});
it("should CRUD a function registration", (done) => {
// Create
Parse.Hooks.createFunction("My-Test-Function", "http://someurl").then((res) => {
expect(res.functionName).toBe("My-Test-Function");
expect(res.url).toBe("http://someurl")
// Find
return Parse.Hooks.getFunction("My-Test-Function");
}, (err) => {
fail(err);
done();
}).then((res) => {
expect(res).not.toBe(null);
expect(res).not.toBe(undefined);
expect(res.url).toBe("http://someurl");
// delete
return Parse.Hooks.updateFunction("My-Test-Function", "http://anotherurl");
}, (err) => {
fail(err);
done();
}).then((res) => {
expect(res.functionName).toBe("My-Test-Function");
expect(res.url).toBe("http://anotherurl")
return Parse.Hooks.deleteFunction("My-Test-Function");
}, (err) => {
fail(err);
done();
}).then((res) => {
// Find again! but should be deleted
return Parse.Hooks.getFunction("My-Test-Function");
}, (err) => {
fail(err);
done();
}).then((res) => {
fail("Should not succeed")
done();
}, (err) => {
expect(err).not.toBe(null);
expect(err).not.toBe(undefined);
expect(err.code).toBe(143);
expect(err.error).toBe("no function named: My-Test-Function is defined")
done();
})
});
it("should CRUD a trigger registration", (done) => {
// Create
Parse.Hooks.createTrigger("MyClass","beforeDelete", "http://someurl").then((res) => {
expect(res.className).toBe("MyClass");
expect(res.triggerName).toBe("beforeDelete");
expect(res.url).toBe("http://someurl")
// Find
return Parse.Hooks.getTrigger("MyClass","beforeDelete");
}, (err) => {
fail(err);
done();
}).then((res) => {
expect(res).not.toBe(null);
expect(res).not.toBe(undefined);
expect(res.url).toBe("http://someurl");
// delete
return Parse.Hooks.updateTrigger("MyClass","beforeDelete", "http://anotherurl");
}, (err) => {
fail(err);
done();
}).then((res) => {
expect(res.className).toBe("MyClass");
expect(res.url).toBe("http://anotherurl")
return Parse.Hooks.deleteTrigger("MyClass","beforeDelete");
}, (err) => {
fail(err);
done();
}).then((res) => {
// Find again! but should be deleted
return Parse.Hooks.getTrigger("MyClass","beforeDelete");
}, (err) => {
fail(err);
done();
}).then(function(){
fail("should not succeed");
done();
}, (err) => {
expect(err).not.toBe(null);
expect(err).not.toBe(undefined);
expect(err.code).toBe(143);
expect(err.error).toBe("class MyClass does not exist")
done();
});
});
it("should fail to register hooks without Master Key", (done) => {
request.post(Parse.serverURL+"/hooks/functions", {
headers: {
"X-Parse-Application-Id": Parse.applicationId,
"X-Parse-REST-API-Key": Parse.restKey,
},
body: JSON.stringify({ url: "http://hello.word", functionName: "SomeFunction"})
}, (err, res, body) => {
body = JSON.parse(body);
expect(body.error).toBe("unauthorized");
done();
})
});
it("should fail trying to create two times the same function", (done) => {
Parse.Hooks.createFunction("my_new_function", "http://url.com").then( () => {
return Parse.Hooks.createFunction("my_new_function", "http://url.com")
}, () => {
fail("should create a new function");
}).then( () => {
fail("should not be able to create the same function");
}, (err) => {
expect(err).not.toBe(undefined);
expect(err).not.toBe(null);
expect(err.code).toBe(143);
expect(err.error).toBe('function name: my_new_function already exits')
return Parse.Hooks.deleteFunction("my_new_function");
}).then(() => {
done();
}, (err) => {
fail(err);
done();
})
});
it("should fail trying to create two times the same trigger", (done) => {
Parse.Hooks.createTrigger("MyClass", "beforeSave", "http://url.com").then( () => {
return Parse.Hooks.createTrigger("MyClass", "beforeSave", "http://url.com")
}, () => {
fail("should create a new trigger");
}).then( () => {
fail("should not be able to create the same trigger");
}, (err) => {
expect(err.code).toBe(143);
expect(err.error).toBe('class MyClass already has trigger beforeSave')
return Parse.Hooks.deleteTrigger("MyClass", "beforeSave");
}).then(() => {
done();
}, (err) => {
fail(err);
done();
})
});
it("should fail trying to update a function that don't exist", (done) => {
Parse.Hooks.updateFunction("A_COOL_FUNCTION", "http://url.com").then( () => {
fail("Should not succeed")
}, (err) => {
expect(err.code).toBe(143);
expect(err.error).toBe('no function named: A_COOL_FUNCTION is defined');
return Parse.Hooks.getFunction("A_COOL_FUNCTION")
}).then( (res) => {
fail("the function should not exist");
done();
}, (err) => {
expect(err.code).toBe(143);
expect(err.error).toBe('no function named: A_COOL_FUNCTION is defined');
done();
});
});
it("should fail trying to update a trigger that don't exist", (done) => {
Parse.Hooks.updateTrigger("AClassName","beforeSave", "http://url.com").then( () => {
fail("Should not succeed")
}, (err) => {
expect(err.code).toBe(143);
expect(err.error).toBe('class AClassName does not exist');
return Parse.Hooks.getTrigger("AClassName","beforeSave")
}).then( (res) => {
fail("the function should not exist");
done();
}, (err) => {
expect(err.code).toBe(143);
expect(err.error).toBe('class AClassName does not exist');
done();
});
});
it("should fail trying to create a malformed function", (done) => {
Parse.Hooks.createFunction("MyFunction").then( (res) => {
fail(res);
}, (err) => {
expect(err.code).toBe(143);
expect(err.error).toBe("invalid hook declaration");
done();
});
});
it("should fail trying to create a malformed function (REST)", (done) => {
request.post(Parse.serverURL+"/hooks/functions", {
headers: {
"X-Parse-Application-Id": Parse.applicationId,
"X-Parse-Master-Key": Parse.masterKey,
},
body: JSON.stringify({ functionName: "SomeFunction"})
}, (err, res, body) => {
body = JSON.parse(body);
expect(body.error).toBe("invalid hook declaration");
expect(body.code).toBe(143);
done();
})
});
it("should create hooks and properly preload them", (done) => {
var promises = [];
for (var i = 0; i<5; i++) {
promises.push(Parse.Hooks.createTrigger("MyClass"+i, "beforeSave", "http://url.com/beforeSave/"+i));
promises.push(Parse.Hooks.createFunction("AFunction"+i, "http://url.com/function"+i));
}
Parse.Promise.when(promises).then(function(results){
for (var i=0; i<5; i++) {
// Delete everything from memory, as the server just started
triggers.removeTrigger("beforeSave", "MyClass"+i, Parse.applicationId);
triggers.removeFunction("AFunction"+i, Parse.applicationId);
expect(triggers.getTrigger("MyClass"+i, "beforeSave", Parse.applicationId)).toBeUndefined();
expect(triggers.getFunction("AFunction"+i, Parse.applicationId)).toBeUndefined();
}
const hooksController = new HooksController(Parse.applicationId);
return hooksController.load()
}, (err) => {
console.error(err);
fail();
done();
}).then(function() {
for (var i=0; i<5; i++) {
expect(triggers.getTrigger("MyClass"+i, "beforeSave", Parse.applicationId)).not.toBeUndefined();
expect(triggers.getFunction("AFunction"+i, Parse.applicationId)).not.toBeUndefined();
}
done();
}, (err) => {
console.error(err);
fail();
done();
})
});
it("should run the function on the test server", (done) => {
app.post("/SomeFunction", function(req, res) {
res.json({success:"OK!"});
});
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/SomeFunction").then(function(){
return Parse.Cloud.run("SOME_TEST_FUNCTION")
}, (err) => {
console.error(err);
fail("Should not fail creating a function");
done();
}).then(function(res){
expect(res).toBe("OK!");
done();
}, (err) => {
console.error(err);
fail("Should not fail calling a function");
done();
})
});
it("should run the function on the test server", (done) => {
app.post("/SomeFunctionError", function(req, res) {
res.json({error: {code: 1337, error: "hacking that one!"}});
});
// The function is delete as the DB is dropped between calls
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/SomeFunctionError").then(function(){
return Parse.Cloud.run("SOME_TEST_FUNCTION")
}, (err) => {
console.error(err);
fail("Should not fail creating a function");
done();
}).then(function(res){
fail("Should not succeed calling that function");
done();
}, (err) => {
expect(err.code).toBe(141);
expect(err.message.code).toEqual(1337)
expect(err.message.error).toEqual("hacking that one!");
done();
});
});
it("should run the beforeSave hook on the test server", (done) => {
var triggerCount = 0;
app.post("/BeforeSaveSome", function(req, res) {
triggerCount++;
var object = req.body.object;
object.hello = "world";
// Would need parse cloud express to set much more
// But this should override the key upon return
res.json({success: {object: object}});
});
// The function is delete as the DB is dropped between calls
Parse.Hooks.createTrigger("SomeRandomObject", "beforeSave" ,hookServerURL+"/BeforeSaveSome").then(function(){
const obj = new Parse.Object("SomeRandomObject");
return obj.save();
}).then(function(res){
expect(triggerCount).toBe(1);
return res.fetch();
}).then(function(res){
expect(res.get("hello")).toEqual("world");
done();
}).fail((err) => {
console.error(err);
fail("Should not fail creating a function");
done();
});
});
it("should run the afterSave hook on the test server", (done) => {
var triggerCount = 0;
var newObjectId;
app.post("/AfterSaveSome", function(req, res) {
triggerCount++;
var obj = new Parse.Object("AnotherObject");
obj.set("foo", "bar");
obj.save().then(function(obj){
newObjectId = obj.id;
res.json({success: {}});
})
});
// The function is delete as the DB is dropped between calls
Parse.Hooks.createTrigger("SomeRandomObject", "afterSave" ,hookServerURL+"/AfterSaveSome").then(function(){
const obj = new Parse.Object("SomeRandomObject");
return obj.save();
}).then(function(res){
var promise = new Parse.Promise();
// Wait a bit here as it's an after save
setTimeout(function(){
expect(triggerCount).toBe(1);
var q = new Parse.Query("AnotherObject");
q.get(newObjectId).then(function(r){
promise.resolve(r);
});
}, 300)
return promise;
}).then(function(res){
expect(res.get("foo")).toEqual("bar");
done();
}).fail((err) => {
console.error(err);
fail("Should not fail creating a function");
done();
});
});
});
+92 -1
View File
@@ -1,3 +1,4 @@
'use strict';
// These tests check the Installations functionality of the REST API.
// Ported from installation_collection_test.go
@@ -9,7 +10,7 @@ var Parse = require('parse/node').Parse;
var rest = require('../src/rest');
var config = new Config('test');
var database = DatabaseAdapter.getDatabaseConnection('test');
let database = DatabaseAdapter.getDatabaseConnection('test', 'test_');
describe('Installations', () => {
@@ -118,6 +119,50 @@ describe('Installations', () => {
}).catch((error) => { console.log(error); });
});
it('should properly fail queying installations', (done) => {
var installId = '12345678-abcd-abcd-abcd-123456789abc';
var device = 'android';
var input = {
'installationId': installId,
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
let query = new Parse.Query(Parse.Installation);
return query.find()
}).then((results) => {
fail('Should not succeed!');
done();
}).catch((error) => {
expect(error.code).toBe(119);
expect(error.message).toBe('Clients aren\'t allowed to perform the find operation on the installation collection.')
done();
});
});
it('should properly queying installations with masterKey', (done) => {
var installId = '12345678-abcd-abcd-abcd-123456789abc';
var device = 'android';
var input = {
'installationId': installId,
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
let query = new Parse.Query(Parse.Installation);
return query.find({useMasterKey: true});
}).then((results) => {
expect(results.length).toEqual(1);
var obj = results[0].toJSON();
expect(obj.installationId).toEqual(installId);
expect(obj.deviceType).toEqual(device);
done();
}).catch((error) => {
fail('Should not fail');
done();
});
});
it('fails with missing ids', (done) => {
var input = {
'deviceType': 'android',
@@ -445,6 +490,52 @@ describe('Installations', () => {
});
});
it('update android device token with duplicate device token', (done) => {
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
var input = {
'installationId': installId1,
'deviceToken': t,
'deviceType': 'android'
};
var firstObject;
var secondObject;
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
input = {
'installationId': installId2,
'deviceType': 'android'
};
return rest.create(config, auth.nobody(config), '_Installation', input);
}).then(() => {
return database.mongoFind('_Installation',
{installationId: installId1}, {});
}).then((results) => {
expect(results.length).toEqual(1);
firstObject = results[0];
return database.mongoFind('_Installation',
{installationId: installId2}, {});
}).then((results) => {
expect(results.length).toEqual(1);
secondObject = results[0];
// Update second installation to conflict with first installation
input = {
'objectId': secondObject._id,
'deviceToken': t
};
return rest.update(config, auth.nobody(config), '_Installation',
secondObject._id, input);
}).then(() => {
// The first object should have been deleted
return database.mongoFind('_Installation', {_id: firstObject._id}, {});
}).then((results) => {
expect(results.length).toEqual(0);
done();
}).catch((error) => { console.log(error); });
});
it('update ios device token with duplicate device token', (done) => {
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
+962
View File
@@ -0,0 +1,962 @@
var Parse = require('parse/node');
var ParseLiveQueryServer = require('../src/LiveQuery/ParseLiveQueryServer').ParseLiveQueryServer;
// Global mock info
var queryHashValue = 'hash';
var testUserId = 'userId';
var testClassName = 'TestObject';
describe('ParseLiveQueryServer', function() {
beforeEach(function(done) {
// Mock ParseWebSocketServer
var mockParseWebSocketServer = jasmine.createSpy('ParseWebSocketServer');
jasmine.mockLibrary('../src/LiveQuery/ParseWebSocketServer', 'ParseWebSocketServer', mockParseWebSocketServer);
// Mock Client
var mockClient = function() {
this.pushConnect = jasmine.createSpy('pushConnect');
this.pushSubscribe = jasmine.createSpy('pushSubscribe');
this.pushUnsubscribe = jasmine.createSpy('pushUnsubscribe');
this.pushDelete = jasmine.createSpy('pushDelete');
this.pushCreate = jasmine.createSpy('pushCreate');
this.pushEnter = jasmine.createSpy('pushEnter');
this.pushUpdate = jasmine.createSpy('pushUpdate');
this.pushLeave = jasmine.createSpy('pushLeave');
this.addSubscriptionInfo = jasmine.createSpy('addSubscriptionInfo');
this.getSubscriptionInfo = jasmine.createSpy('getSubscriptionInfo');
this.deleteSubscriptionInfo = jasmine.createSpy('deleteSubscriptionInfo');
}
mockClient.pushError = jasmine.createSpy('pushError');
jasmine.mockLibrary('../src/LiveQuery/Client', 'Client', mockClient);
// Mock Subscription
var mockSubscriotion = function() {
this.addClientSubscription = jasmine.createSpy('addClientSubscription');
this.deleteClientSubscription = jasmine.createSpy('deleteClientSubscription');
}
jasmine.mockLibrary('../src/LiveQuery/Subscription', 'Subscription', mockSubscriotion);
// Mock queryHash
var mockQueryHash = jasmine.createSpy('matchesQuery').and.returnValue(queryHashValue);
jasmine.mockLibrary('../src/LiveQuery/QueryTools', 'queryHash', mockQueryHash);
// Mock matchesQuery
var mockMatchesQuery = jasmine.createSpy('matchesQuery').and.returnValue(true);
jasmine.mockLibrary('../src/LiveQuery/QueryTools', 'matchesQuery', mockMatchesQuery);
// Mock tv4
var mockValidate = function() {
return true;
}
jasmine.mockLibrary('tv4', 'validate', mockValidate);
// Mock ParsePubSub
var mockParsePubSub = {
createPublisher: function() {
return {
publish: jasmine.createSpy('publish'),
on: jasmine.createSpy('on')
}
},
createSubscriber: function() {
return {
subscribe: jasmine.createSpy('subscribe'),
on: jasmine.createSpy('on')
}
}
};
jasmine.mockLibrary('../src/LiveQuery/ParsePubSub', 'ParsePubSub', mockParsePubSub);
// Make mock SessionTokenCache
var mockSessionTokenCache = function(){
this.getUserId = function(sessionToken){
if (typeof sessionToken === 'undefined') {
return Parse.Promise.as(undefined);
}
if (sessionToken === null) {
return Parse.Promise.error();
}
return Parse.Promise.as(testUserId);
};
};
jasmine.mockLibrary('../src/LiveQuery/SessionTokenCache', 'SessionTokenCache', mockSessionTokenCache);
done();
});
it('can be initialized', function() {
var httpServer = {};
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, httpServer);
expect(parseLiveQueryServer.clientId).toBe(0);
expect(parseLiveQueryServer.clients.size).toBe(0);
expect(parseLiveQueryServer.subscriptions.size).toBe(0);
});
it('can handle connect command', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var parseWebSocket = {
clientId: -1
};
parseLiveQueryServer._validateKeys = jasmine.createSpy('validateKeys').and.returnValue(true);
parseLiveQueryServer._handleConnect(parseWebSocket);
expect(parseLiveQueryServer.clientId).toBe(1);
expect(parseWebSocket.clientId).toBe(0);
var client = parseLiveQueryServer.clients.get(0);
expect(client).not.toBeNull();
// Make sure we send connect response to the client
expect(client.pushConnect).toHaveBeenCalled();
});
it('can handle subscribe command without clientId', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var incompleteParseConn = {
};
parseLiveQueryServer._handleSubscribe(incompleteParseConn, {});
var Client = require('../src/LiveQuery/Client').Client;
expect(Client.pushError).toHaveBeenCalled();
});
it('can handle subscribe command with new query', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Add mock client
var clientId = 1;
var client = addMockClient(parseLiveQueryServer, clientId);
// Handle mock subscription
var parseWebSocket = {
clientId: clientId
};
var query = {
className: 'test',
where: {
key: 'value'
},
fields: [ 'test' ]
}
var requestId = 2;
var request = {
query: query,
requestId: requestId,
sessionToken: 'sessionToken'
}
parseLiveQueryServer._handleSubscribe(parseWebSocket, request);
// Make sure we add the subscription to the server
var subscriptions = parseLiveQueryServer.subscriptions;
expect(subscriptions.size).toBe(1);
expect(subscriptions.get(query.className)).not.toBeNull();
var classSubscriptions = subscriptions.get(query.className);
expect(classSubscriptions.size).toBe(1);
expect(classSubscriptions.get('hash')).not.toBeNull();
// TODO(check subscription constructor to verify we pass the right argument)
// Make sure we add clientInfo to the subscription
var subscription = classSubscriptions.get('hash');
expect(subscription.addClientSubscription).toHaveBeenCalledWith(clientId, requestId);
// Make sure we add subscriptionInfo to the client
var args = client.addSubscriptionInfo.calls.first().args;
expect(args[0]).toBe(requestId);
expect(args[1].fields).toBe(query.fields);
expect(args[1].sessionToken).toBe(request.sessionToken);
// Make sure we send subscribe response to the client
expect(client.pushSubscribe).toHaveBeenCalledWith(requestId);
});
it('can handle subscribe command with existing query', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Add two mock clients
var clientId = 1;
var client = addMockClient(parseLiveQueryServer, clientId);
var clientIdAgain = 2;
var clientAgain = addMockClient(parseLiveQueryServer, clientIdAgain);
// Add subscription for mock client 1
var parseWebSocket = {
clientId: clientId
};
var requestId = 2;
var query = {
className: 'test',
where: {
key: 'value'
},
fields: [ 'test' ]
}
addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query);
// Add subscription for mock client 2
var parseWebSocketAgain = {
clientId: clientIdAgain
};
var queryAgain = {
className: 'test',
where: {
key: 'value'
},
fields: [ 'testAgain' ]
}
var requestIdAgain = 1;
addMockSubscription(parseLiveQueryServer, clientIdAgain, requestIdAgain, parseWebSocketAgain, queryAgain);
// Make sure we only have one subscription
var subscriptions = parseLiveQueryServer.subscriptions;
expect(subscriptions.size).toBe(1);
expect(subscriptions.get(query.className)).not.toBeNull();
var classSubscriptions = subscriptions.get(query.className);
expect(classSubscriptions.size).toBe(1);
expect(classSubscriptions.get('hash')).not.toBeNull();
// Make sure we add clientInfo to the subscription
var subscription = classSubscriptions.get('hash');
// Make sure client 2 info has been added
var args = subscription.addClientSubscription.calls.mostRecent().args;
expect(args).toEqual([clientIdAgain, requestIdAgain]);
// Make sure we add subscriptionInfo to the client 2
args = clientAgain.addSubscriptionInfo.calls.mostRecent().args;
expect(args[0]).toBe(requestIdAgain);
expect(args[1].fields).toBe(queryAgain.fields);
});
it('can handle unsubscribe command without clientId', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var incompleteParseConn = {
};
parseLiveQueryServer._handleUnsubscribe(incompleteParseConn, {});
var Client = require('../src/LiveQuery/Client').Client;
expect(Client.pushError).toHaveBeenCalled();
});
it('can handle unsubscribe command without not existed client', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var parseWebSocket = {
clientId: 1
};
parseLiveQueryServer._handleUnsubscribe(parseWebSocket, {});
var Client = require('../src/LiveQuery/Client').Client;
expect(Client.pushError).toHaveBeenCalled();
});
it('can handle unsubscribe command without not existed query', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Add mock client
var clientId = 1;
var client = addMockClient(parseLiveQueryServer, clientId);
// Handle unsubscribe command
var parseWebSocket = {
clientId: 1
};
parseLiveQueryServer._handleUnsubscribe(parseWebSocket, {});
var Client = require('../src/LiveQuery/Client').Client;
expect(Client.pushError).toHaveBeenCalled();
});
it('can handle unsubscribe command', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Add mock client
var clientId = 1;
var client = addMockClient(parseLiveQueryServer, clientId);
// Add subscription for mock client
var parseWebSocket = {
clientId: 1
};
var requestId = 2;
var subscription = addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket);
// Mock client.getSubscriptionInfo
var subscriptionInfo = client.addSubscriptionInfo.calls.mostRecent().args[1];
client.getSubscriptionInfo = function() {
return subscriptionInfo;
};
// Handle unsubscribe command
var requestAgain = {
requestId: requestId
};
parseLiveQueryServer._handleUnsubscribe(parseWebSocket, requestAgain);
// Make sure we delete subscription from client
expect(client.deleteSubscriptionInfo).toHaveBeenCalledWith(requestId);
// Make sure we delete client from subscription
expect(subscription.deleteClientSubscription).toHaveBeenCalledWith(clientId, requestId);
// Make sure we clear subscription in the server
var subscriptions = parseLiveQueryServer.subscriptions;
expect(subscriptions.size).toBe(0);
});
it('can set connect command message handler for a parseWebSocket', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Register mock connect/subscribe/unsubscribe handler for the server
parseLiveQueryServer._handleConnect = jasmine.createSpy('_handleSubscribe');
// Make mock parseWebsocket
var EventEmitter = require('events');
var parseWebSocket = new EventEmitter();
// Register message handlers for the parseWebSocket
parseLiveQueryServer._onConnect(parseWebSocket);
// Check connect request
var connectRequest = {
op: 'connect'
};
// Trigger message event
parseWebSocket.emit('message', connectRequest);
// Make sure _handleConnect is called
var args = parseLiveQueryServer._handleConnect.calls.mostRecent().args;
expect(args[0]).toBe(parseWebSocket);
});
it('can set subscribe command message handler for a parseWebSocket', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Register mock connect/subscribe/unsubscribe handler for the server
parseLiveQueryServer._handleSubscribe = jasmine.createSpy('_handleSubscribe');
// Make mock parseWebsocket
var EventEmitter = require('events');
var parseWebSocket = new EventEmitter();
// Register message handlers for the parseWebSocket
parseLiveQueryServer._onConnect(parseWebSocket);
// Check subscribe request
var subscribeRequest = '{"op":"subscribe"}';
// Trigger message event
parseWebSocket.emit('message', subscribeRequest);
// Make sure _handleSubscribe is called
var args = parseLiveQueryServer._handleSubscribe.calls.mostRecent().args;
expect(args[0]).toBe(parseWebSocket);
expect(JSON.stringify(args[1])).toBe(subscribeRequest);
});
it('can set unsubscribe command message handler for a parseWebSocket', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Register mock connect/subscribe/unsubscribe handler for the server
parseLiveQueryServer._handleUnsubscribe = jasmine.createSpy('_handleSubscribe');
// Make mock parseWebsocket
var EventEmitter = require('events');
var parseWebSocket = new EventEmitter();
// Register message handlers for the parseWebSocket
parseLiveQueryServer._onConnect(parseWebSocket);
// Check unsubscribe request
var unsubscribeRequest = '{"op":"unsubscribe"}';
// Trigger message event
parseWebSocket.emit('message', unsubscribeRequest);
// Make sure _handleUnsubscribe is called
var args = parseLiveQueryServer._handleUnsubscribe.calls.mostRecent().args;
expect(args[0]).toBe(parseWebSocket);
expect(JSON.stringify(args[1])).toBe(unsubscribeRequest);
});
it('can set unknown command message handler for a parseWebSocket', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock parseWebsocket
var EventEmitter = require('events');
var parseWebSocket = new EventEmitter();
// Register message handlers for the parseWebSocket
parseLiveQueryServer._onConnect(parseWebSocket);
// Check unknown request
var unknownRequest = '{"op":"unknown"}';
// Trigger message event
parseWebSocket.emit('message', unknownRequest);
var Client = require('../src/LiveQuery/Client').Client;
expect(Client.pushError).toHaveBeenCalled();
});
it('can set disconnect command message handler for a parseWebSocket which has not registered to the server', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var EventEmitter = require('events');
var parseWebSocket = new EventEmitter();
parseWebSocket.clientId = 1;
// Register message handlers for the parseWebSocket
parseLiveQueryServer._onConnect(parseWebSocket);
// Make sure we do not crash
// Trigger disconnect event
parseWebSocket.emit('disconnect');
});
// TODO: Test server can set disconnect command message handler for a parseWebSocket
it('has no subscription and can handle object delete command', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make deletedParseObject
var parseObject = new Parse.Object(testClassName);
parseObject._finishFetch({
key: 'value',
className: testClassName
});
// Make mock message
var message = {
currentParseObject: parseObject
};
// Make sure we do not crash in this case
parseLiveQueryServer._onAfterDelete(message, {});
});
it('can handle object delete command which does not match any subscription', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make deletedParseObject
var parseObject = new Parse.Object(testClassName);
parseObject._finishFetch({
key: 'value',
className: testClassName
});
// Make mock message
var message = {
currentParseObject: parseObject
};
// Add mock client
var clientId = 1;
addMockClient(parseLiveQueryServer, clientId);
// Add mock subscription
var requestId = 2;
addMockSubscription(parseLiveQueryServer, clientId, requestId);
var client = parseLiveQueryServer.clients.get(clientId);
// Mock _matchesSubscription to return not matching
parseLiveQueryServer._matchesSubscription = function() {
return false;
};
parseLiveQueryServer._matchesACL = function() {
return true;
};
parseLiveQueryServer._onAfterDelete(message);
// Make sure we do not send command to client
expect(client.pushDelete).not.toHaveBeenCalled();
});
it('can handle object delete command which matches some subscriptions', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make deletedParseObject
var parseObject = new Parse.Object(testClassName);
parseObject._finishFetch({
key: 'value',
className: testClassName
});
// Make mock message
var message = {
currentParseObject: parseObject
};
// Add mock client
var clientId = 1;
addMockClient(parseLiveQueryServer, clientId);
// Add mock subscription
var requestId = 2;
addMockSubscription(parseLiveQueryServer, clientId, requestId);
var client = parseLiveQueryServer.clients.get(clientId);
// Mock _matchesSubscription to return matching
parseLiveQueryServer._matchesSubscription = function() {
return true;
};
parseLiveQueryServer._matchesACL = function() {
return Parse.Promise.as(true);
};
parseLiveQueryServer._onAfterDelete(message);
// Make sure we send command to client, since _matchesACL is async, we have to
// wait and check
setTimeout(function() {
expect(client.pushDelete).toHaveBeenCalled();
done();
}, jasmine.ASYNC_TEST_WAIT_TIME);
});
it('has no subscription and can handle object save command', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock request message
var message = generateMockMessage();
// Make sure we do not crash in this case
parseLiveQueryServer._onAfterSave(message);
});
it('can handle object save command which does not match any subscription', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock request message
var message = generateMockMessage();
// Add mock client
var clientId = 1;
var client = addMockClient(parseLiveQueryServer, clientId);
// Add mock subscription
var requestId = 2;
addMockSubscription(parseLiveQueryServer, clientId, requestId);
// Mock _matchesSubscription to return not matching
parseLiveQueryServer._matchesSubscription = function() {
return false;
};
parseLiveQueryServer._matchesACL = function() {
return Parse.Promise.as(true)
};
// Trigger onAfterSave
parseLiveQueryServer._onAfterSave(message);
// Make sure we do not send command to client
setTimeout(function(){
expect(client.pushCreate).not.toHaveBeenCalled();
expect(client.pushEnter).not.toHaveBeenCalled();
expect(client.pushUpdate).not.toHaveBeenCalled();
expect(client.pushDelete).not.toHaveBeenCalled();
expect(client.pushLeave).not.toHaveBeenCalled();
done();
}, jasmine.ASYNC_TEST_WAIT_TIME);
});
it('can handle object enter command which matches some subscriptions', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock request message
var message = generateMockMessage(true);
// Add mock client
var clientId = 1;
var client = addMockClient(parseLiveQueryServer, clientId);
// Add mock subscription
var requestId = 2;
addMockSubscription(parseLiveQueryServer, clientId, requestId);
// Mock _matchesSubscription to return matching
// In order to mimic a enter, we need original match return false
// and the current match return true
var counter = 0;
parseLiveQueryServer._matchesSubscription = function(parseObject, subscription){
if (!parseObject) {
return false;
}
counter += 1;
return counter % 2 === 0;
};
parseLiveQueryServer._matchesACL = function() {
return Parse.Promise.as(true)
};
parseLiveQueryServer._onAfterSave(message);
// Make sure we send enter command to client
setTimeout(function(){
expect(client.pushCreate).not.toHaveBeenCalled();
expect(client.pushEnter).toHaveBeenCalled();
expect(client.pushUpdate).not.toHaveBeenCalled();
expect(client.pushDelete).not.toHaveBeenCalled();
expect(client.pushLeave).not.toHaveBeenCalled();
done();
}, jasmine.ASYNC_TEST_WAIT_TIME);
});
it('can handle object update command which matches some subscriptions', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock request message
var message = generateMockMessage(true);
// Add mock client
var clientId = 1;
var client = addMockClient(parseLiveQueryServer, clientId);
// Add mock subscription
var requestId = 2;
addMockSubscription(parseLiveQueryServer, clientId, requestId);
// Mock _matchesSubscription to return matching
parseLiveQueryServer._matchesSubscription = function(parseObject, subscription){
if (!parseObject) {
return false;
}
return true;
};
parseLiveQueryServer._matchesACL = function() {
return Parse.Promise.as(true)
};
parseLiveQueryServer._onAfterSave(message);
// Make sure we send update command to client
setTimeout(function(){
expect(client.pushCreate).not.toHaveBeenCalled();
expect(client.pushEnter).not.toHaveBeenCalled();
expect(client.pushUpdate).toHaveBeenCalled();
expect(client.pushDelete).not.toHaveBeenCalled();
expect(client.pushLeave).not.toHaveBeenCalled();
done();
}, jasmine.ASYNC_TEST_WAIT_TIME);
});
it('can handle object leave command which matches some subscriptions', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock request message
var message = generateMockMessage(true);
// Add mock client
var clientId = 1;
var client = addMockClient(parseLiveQueryServer, clientId);
// Add mock subscription
var requestId = 2;
addMockSubscription(parseLiveQueryServer, clientId, requestId);
// Mock _matchesSubscription to return matching
// In order to mimic a leave, we need original match return true
// and the current match return false
var counter = 0;
parseLiveQueryServer._matchesSubscription = function(parseObject, subscription){
if (!parseObject) {
return false;
}
counter += 1;
return counter % 2 !== 0;
};
parseLiveQueryServer._matchesACL = function() {
return Parse.Promise.as(true)
};
parseLiveQueryServer._onAfterSave(message);
// Make sure we send leave command to client
setTimeout(function(){
expect(client.pushCreate).not.toHaveBeenCalled();
expect(client.pushEnter).not.toHaveBeenCalled();
expect(client.pushUpdate).not.toHaveBeenCalled();
expect(client.pushDelete).not.toHaveBeenCalled();
expect(client.pushLeave).toHaveBeenCalled();
done();
}, jasmine.ASYNC_TEST_WAIT_TIME);
});
it('can handle object create command which matches some subscriptions', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock request message
var message = generateMockMessage();
// Add mock client
var clientId = 1;
var client = addMockClient(parseLiveQueryServer, clientId);
// Add mock subscription
var requestId = 2;
addMockSubscription(parseLiveQueryServer, clientId, requestId);
// Mock _matchesSubscription to return matching
parseLiveQueryServer._matchesSubscription = function(parseObject, subscription){
if (!parseObject) {
return false;
}
return true;
};
parseLiveQueryServer._matchesACL = function() {
return Parse.Promise.as(true)
};
parseLiveQueryServer._onAfterSave(message);
// Make sure we send create command to client
setTimeout(function(){
expect(client.pushCreate).toHaveBeenCalled();
expect(client.pushEnter).not.toHaveBeenCalled();
expect(client.pushUpdate).not.toHaveBeenCalled();
expect(client.pushDelete).not.toHaveBeenCalled();
expect(client.pushLeave).not.toHaveBeenCalled();
done();
}, jasmine.ASYNC_TEST_WAIT_TIME);
});
it('can match subscription for null or undefined parse object', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock subscription
var subscription = {
match: jasmine.createSpy('match')
}
expect(parseLiveQueryServer._matchesSubscription(null, subscription)).toBe(false);
expect(parseLiveQueryServer._matchesSubscription(undefined, subscription)).toBe(false);
// Make sure subscription.match is not called
expect(subscription.match).not.toHaveBeenCalled();
});
it('can match subscription', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock subscription
var subscription = {
query: {}
}
var parseObject = {};
expect(parseLiveQueryServer._matchesSubscription(parseObject, subscription)).toBe(true);
// Make sure matchesQuery is called
var matchesQuery = require('../src/LiveQuery/QueryTools').matchesQuery;
expect(matchesQuery).toHaveBeenCalledWith(parseObject, subscription.query);
});
it('can inflate parse object', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock request
var objectJSON = {
"className":"testClassName",
"createdAt":"2015-12-22T01:51:12.955Z",
"key":"value",
"objectId":"BfwxBCz6yW",
"updatedAt":"2016-01-05T00:46:45.659Z"
};
var originalObjectJSON = {
"className":"testClassName",
"createdAt":"2015-12-22T01:51:12.955Z",
"key":"originalValue",
"objectId":"BfwxBCz6yW",
"updatedAt":"2016-01-05T00:46:45.659Z"
};
var message = {
currentParseObject: objectJSON,
originalParseObject: originalObjectJSON
};
// Inflate the object
parseLiveQueryServer._inflateParseObject(message);
// Verify object
var object = message.currentParseObject;
expect(object instanceof Parse.Object).toBeTruthy();
expect(object.get('key')).toEqual('value');
expect(object.className).toEqual('testClassName');
expect(object.id).toBe('BfwxBCz6yW');
expect(object.createdAt).not.toBeUndefined();
expect(object.updatedAt).not.toBeUndefined();
// Verify original object
var originalObject = message.originalParseObject;
expect(originalObject instanceof Parse.Object).toBeTruthy();
expect(originalObject.get('key')).toEqual('originalValue');
expect(originalObject.className).toEqual('testClassName');
expect(originalObject.id).toBe('BfwxBCz6yW');
expect(originalObject.createdAt).not.toBeUndefined();
expect(originalObject.updatedAt).not.toBeUndefined();
});
it('can match undefined ACL', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var client = {};
var requestId = 0;
parseLiveQueryServer._matchesACL(undefined, client, requestId).then(function(isMatched) {
expect(isMatched).toBe(true);
done();
});
});
it('can match ACL with none exist requestId', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var acl = new Parse.ACL();
var client = {
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue(undefined)
};
var requestId = 0;
var isChecked = false;
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
expect(isMatched).toBe(false);
done();
});
});
it('can match ACL with public read access', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var acl = new Parse.ACL();
acl.setPublicReadAccess(true);
var client = {
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
sessionToken: 'sessionToken'
})
};
var requestId = 0;
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
expect(isMatched).toBe(true);
done();
});
});
it('can match ACL with valid subscription sessionToken', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var acl = new Parse.ACL();
acl.setReadAccess(testUserId, true);
var client = {
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
sessionToken: 'sessionToken'
})
};
var requestId = 0;
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
expect(isMatched).toBe(true);
done();
});
});
it('can match ACL with valid client sessionToken', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var acl = new Parse.ACL();
acl.setReadAccess(testUserId, true);
// Mock sessionTokenCache will return false when sessionToken is undefined
var client = {
sessionToken: 'sessionToken',
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
sessionToken: undefined
})
};
var requestId = 0;
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
expect(isMatched).toBe(true);
done();
});
});
it('can match ACL with invalid subscription and client sessionToken', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var acl = new Parse.ACL();
acl.setReadAccess(testUserId, true);
// Mock sessionTokenCache will return false when sessionToken is undefined
var client = {
sessionToken: undefined,
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
sessionToken: undefined
})
};
var requestId = 0;
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
expect(isMatched).toBe(false);
done();
});
});
it('can match ACL with subscription sessionToken checking error', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var acl = new Parse.ACL();
acl.setReadAccess(testUserId, true);
// Mock sessionTokenCache will return error when sessionToken is null, this is just
// the behaviour of our mock sessionTokenCache, not real sessionTokenCache
var client = {
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
sessionToken: null
})
};
var requestId = 0;
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
expect(isMatched).toBe(false);
done();
});
});
it('can match ACL with client sessionToken checking error', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var acl = new Parse.ACL();
acl.setReadAccess(testUserId, true);
// Mock sessionTokenCache will return error when sessionToken is null
var client = {
sessionToken: null,
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
sessionToken: null
})
};
var requestId = 0;
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
expect(isMatched).toBe(false);
done();
});
});
it('can validate key when valid key is provided', function() {
var parseLiveQueryServer = new ParseLiveQueryServer({}, {
keyPairs: {
clientKey: 'test'
}
});
var request = {
clientKey: 'test'
}
expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).toBeTruthy();
});
it('can validate key when invalid key is provided', function() {
var parseLiveQueryServer = new ParseLiveQueryServer({}, {
keyPairs: {
clientKey: 'test'
}
});
var request = {
clientKey: 'error'
}
expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).not.toBeTruthy();
});
it('can validate key when key is not provided', function() {
var parseLiveQueryServer = new ParseLiveQueryServer({}, {
keyPairs: {
clientKey: 'test'
}
});
var request = {
}
expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).not.toBeTruthy();
});
it('can validate key when validKerPairs is empty', function() {
var parseLiveQueryServer = new ParseLiveQueryServer({}, {});
var request = {
}
expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).toBeTruthy();
});
afterEach(function(){
jasmine.restoreLibrary('../src/LiveQuery/ParseWebSocketServer', 'ParseWebSocketServer');
jasmine.restoreLibrary('../src/LiveQuery/Client', 'Client');
jasmine.restoreLibrary('../src/LiveQuery/Subscription', 'Subscription');
jasmine.restoreLibrary('../src/LiveQuery/QueryTools', 'queryHash');
jasmine.restoreLibrary('../src/LiveQuery/QueryTools', 'matchesQuery');
jasmine.restoreLibrary('tv4', 'validate');
jasmine.restoreLibrary('../src/LiveQuery/ParsePubSub', 'ParsePubSub');
jasmine.restoreLibrary('../src/LiveQuery/SessionTokenCache', 'SessionTokenCache');
});
// Helper functions to add mock client and subscription to a liveQueryServer
function addMockClient(parseLiveQueryServer, clientId) {
var Client = require('../src/LiveQuery/Client').Client;
var client = new Client(clientId, {});
parseLiveQueryServer.clients.set(clientId, client);
return client;
}
function addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query) {
// If parseWebSocket is null, we use the default one
if (!parseWebSocket) {
var EventEmitter = require('events');
parseWebSocket = new EventEmitter();
}
parseWebSocket.clientId = clientId;
// If query is null, we use the default one
if (!query) {
query = {
className: testClassName,
where: {
key: 'value'
},
fields: [ 'test' ]
};
}
var request = {
query: query,
requestId: requestId,
sessionToken: 'sessionToken'
};
parseLiveQueryServer._handleSubscribe(parseWebSocket, request);
// Make mock subscription
var subscription = parseLiveQueryServer.subscriptions.get(query.className).get(queryHashValue);
subscription.hasSubscribingClient = function() {
return false;
}
subscription.className = query.className;
subscription.hash = queryHashValue;
if (subscription.clientRequestIds && subscription.clientRequestIds.has(clientId)) {
subscription.clientRequestIds.get(clientId).push(requestId);
} else {
subscription.clientRequestIds = new Map([[clientId, [requestId]]]);
}
return subscription;
}
// Helper functiosn to generate request message
function generateMockMessage(hasOriginalParseObject) {
var parseObject = new Parse.Object(testClassName);
parseObject._finishFetch({
key: 'value',
className: testClassName
});
var message = {
currentParseObject: parseObject
};
if (hasOriginalParseObject) {
var originalParseObject = new Parse.Object(testClassName);
originalParseObject._finishFetch({
key: 'originalValue',
className: testClassName
});
message.originalParseObject = originalParseObject;
}
return message;
}
});
+150
View File
@@ -1,3 +1,4 @@
"use strict";
// This is a port of the test suite:
// hungry/js/test/parse_object_test.js
//
@@ -336,6 +337,34 @@ describe('Parse.Object testing', () => {
item.save({ "foo^bar": "baz" }).then(fail, done);
});
it("invalid __type", function(done) {
var item = new Parse.Object("Item");
var types = ['Pointer', 'File', 'Date', 'GeoPoint', 'Bytes'];
var Error = Parse.Error;
var tests = types.map(type => {
var test = new Parse.Object("Item");
test.set('foo', {
__type: type
});
return test;
});
var next = function(index) {
if (index < tests.length) {
tests[index].save().then(fail, error => {
expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE);
next(index + 1);
});
} else {
done();
}
}
item.save({
"foo": {
__type: "IvalidName"
}
}).then(fail, err => next(0));
});
it("simple field deletion", function(done) {
var simple = new Parse.Object("SimpleObject");
simple.save({
@@ -571,6 +600,49 @@ describe('Parse.Object testing', () => {
});
});
it("addUnique with object", function(done) {
var x1 = new Parse.Object('X');
x1.set('stuff', [ 1, {'hello': 'world'}, {'foo': 'bar'}]);
x1.save().then(() => {
var objectId = x1.id;
var x2 = new Parse.Object('X', {objectId: objectId});
x2.addUnique('stuff', {'hello': 'world'});
x2.addUnique('stuff', {'bar': 'baz'});
expect(x2.get('stuff')).toEqual([{'hello': 'world'}, {'bar': 'baz'}]);
return x2.save();
}).then(() => {
var query = new Parse.Query('X');
return query.get(x1.id);
}).then((x3) => {
expect(x3.get('stuff')).toEqual([1, {'hello': 'world'}, {'foo': 'bar'}, {'bar': 'baz'}]);
done();
}, (error) => {
fail(error);
done();
});
});
it("removes with object", function(done) {
var x1 = new Parse.Object('X');
x1.set('stuff', [ 1, {'hello': 'world'}, {'foo': 'bar'}]);
x1.save().then(() => {
var objectId = x1.id;
var x2 = new Parse.Object('X', {objectId: objectId});
x2.remove('stuff', {'hello': 'world'});
expect(x2.get('stuff')).toEqual([]);
return x2.save();
}).then(() => {
var query = new Parse.Query('X');
return query.get(x1.id);
}).then((x3) => {
expect(x3.get('stuff')).toEqual([1, {'foo': 'bar'}]);
done();
}, (error) => {
fail(error);
done();
});
});
it("dirty attributes", function(done) {
var object = new Parse.Object("TestObject");
object.set("cat", "good");
@@ -1736,4 +1808,82 @@ describe('Parse.Object testing', () => {
});
});
it("should create nested keys with _", done => {
const object = new Parse.Object("AnObject");
object.set("foo", {
"_bar": "_",
"baz_bar": 1,
"__foo_bar": true,
"_0": "underscore_zero",
"_more": {
"_nested": "key"
}
});
object.save().then( res => {
ok(res);
return res.fetch();
}).then( res => {
const foo = res.get("foo");
expect(foo["_bar"]).toEqual("_");
expect(foo["baz_bar"]).toEqual(1);
expect(foo["__foo_bar"]).toBe(true);
expect(foo["_0"]).toEqual("underscore_zero");
expect(foo["_more"]["_nested"]).toEqual("key");
done();
}).fail( err => {
console.error(err);
fail("should not fail");
done();
});
});
it('should have undefined includes when object is missing', (done) => {
let obj1 = new Parse.Object("AnObject");
let obj2 = new Parse.Object("AnObject");
Parse.Object.saveAll([obj1, obj2]).then(() => {
obj1.set("obj", obj2);
// Save the pointer, delete the pointee
return obj1.save().then(() => { return obj2.destroy() });
}).then(() => {
let query = new Parse.Query("AnObject");
query.include("obj");
return query.find();
}).then((res) => {
expect(res.length).toBe(1);
expect(res[0].get("obj")).toBe(undefined);
let query = new Parse.Query("AnObject");
return query.find();
}).then((res) => {
expect(res.length).toBe(1);
expect(res[0].get("obj")).not.toBe(undefined);
return res[0].get("obj").fetch();
}).then(() => {
fail("Should not fetch a deleted object");
}, (err) => {
expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
done();
})
});
it('should have undefined includes when object is missing on deeper path', (done) => {
let obj1 = new Parse.Object("AnObject");
let obj2 = new Parse.Object("AnObject");
let obj3 = new Parse.Object("AnObject");
Parse.Object.saveAll([obj1, obj2, obj3]).then(() => {
obj1.set("obj", obj2);
obj2.set("obj", obj3);
// Save the pointer, delete the pointee
return Parse.Object.saveAll([obj1, obj2]).then(() => { return obj3.destroy() });
}).then(() => {
let query = new Parse.Query("AnObject");
query.include("obj.obj");
return query.get(obj1.id);
}).then((res) => {
expect(res.get("obj")).not.toBe(undefined);
expect(res.get("obj").get("obj")).toBe(undefined);
done();
});
});
});
+65
View File
@@ -0,0 +1,65 @@
var ParsePubSub = require('../src/LiveQuery/ParsePubSub').ParsePubSub;
describe('ParsePubSub', function() {
beforeEach(function(done) {
// Mock RedisPubSub
var mockRedisPubSub = {
createPublisher: jasmine.createSpy('createPublisherRedis'),
createSubscriber: jasmine.createSpy('createSubscriberRedis')
};
jasmine.mockLibrary('../src/LiveQuery/RedisPubSub', 'RedisPubSub', mockRedisPubSub);
// Mock EventEmitterPubSub
var mockEventEmitterPubSub = {
createPublisher: jasmine.createSpy('createPublisherEventEmitter'),
createSubscriber: jasmine.createSpy('createSubscriberEventEmitter')
};
jasmine.mockLibrary('../src/LiveQuery/EventEmitterPubSub', 'EventEmitterPubSub', mockEventEmitterPubSub);
done();
});
it('can create redis publisher', function() {
var publisher = ParsePubSub.createPublisher({
redisURL: 'redisURL'
});
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createPublisher).toHaveBeenCalledWith('redisURL');
expect(EventEmitterPubSub.createPublisher).not.toHaveBeenCalled();
});
it('can create event emitter publisher', function() {
var publisher = ParsePubSub.createPublisher({});
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createPublisher).not.toHaveBeenCalled();
expect(EventEmitterPubSub.createPublisher).toHaveBeenCalled();
});
it('can create redis subscriber', function() {
var subscriber = ParsePubSub.createSubscriber({
redisURL: 'redisURL'
});
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createSubscriber).toHaveBeenCalledWith('redisURL');
expect(EventEmitterPubSub.createSubscriber).not.toHaveBeenCalled();
});
it('can create event emitter subscriber', function() {
var subscriptionInfos = ParsePubSub.createSubscriber({});
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createSubscriber).not.toHaveBeenCalled();
expect(EventEmitterPubSub.createSubscriber).toHaveBeenCalled();
});
afterEach(function(){
jasmine.restoreLibrary('../src/LiveQuery/RedisPubSub', 'RedisPubSub');
jasmine.restoreLibrary('../src/LiveQuery/EventEmitterPubSub', 'EventEmitterPubSub');
});
});
-150
View File
@@ -1,150 +0,0 @@
var ParsePushAdapter = require('../src/Adapters/Push/ParsePushAdapter');
var APNS = require('../src/APNS');
var GCM = require('../src/GCM');
describe('ParsePushAdapter', () => {
it('can be initialized', (done) => {
// Make mock config
var pushConfig = {
android: {
senderId: 'senderId',
apiKey: 'apiKey'
},
ios: [
{
cert: 'prodCert.pem',
key: 'prodKey.pem',
production: true,
bundleId: 'bundleId'
},
{
cert: 'devCert.pem',
key: 'devKey.pem',
production: false,
bundleId: 'bundleIdAgain'
}
]
};
var parsePushAdapter = new ParsePushAdapter(pushConfig);
// Check ios
var iosSender = parsePushAdapter.senderMap['ios'];
expect(iosSender instanceof APNS).toBe(true);
// Check android
var androidSender = parsePushAdapter.senderMap['android'];
expect(androidSender instanceof GCM).toBe(true);
done();
});
it('can throw on initializing with unsupported push type', (done) => {
// Make mock config
var pushConfig = {
win: {
senderId: 'senderId',
apiKey: 'apiKey'
}
};
expect(function() {
new ParsePushAdapter(pushConfig);
}).toThrow();
done();
});
it('can get valid push types', (done) => {
var parsePushAdapter = new ParsePushAdapter();
expect(parsePushAdapter.getValidPushTypes()).toEqual(['ios', 'android']);
done();
});
it('can classify installation', (done) => {
// Mock installations
var validPushTypes = ['ios', 'android'];
var installations = [
{
deviceType: 'android',
deviceToken: 'androidToken'
},
{
deviceType: 'ios',
deviceToken: 'iosToken'
},
{
deviceType: 'win',
deviceToken: 'winToken'
},
{
deviceType: 'android',
deviceToken: undefined
}
];
var deviceMap = ParsePushAdapter.classifyInstallation(installations, validPushTypes);
expect(deviceMap['android']).toEqual([makeDevice('androidToken')]);
expect(deviceMap['ios']).toEqual([makeDevice('iosToken')]);
expect(deviceMap['win']).toBe(undefined);
done();
});
it('can send push notifications', (done) => {
var parsePushAdapter = new ParsePushAdapter();
// Mock android ios senders
var androidSender = {
send: jasmine.createSpy('send')
};
var iosSender = {
send: jasmine.createSpy('send')
};
var senderMap = {
ios: iosSender,
android: androidSender
};
parsePushAdapter.senderMap = senderMap;
// Mock installations
var installations = [
{
deviceType: 'android',
deviceToken: 'androidToken'
},
{
deviceType: 'ios',
deviceToken: 'iosToken'
},
{
deviceType: 'win',
deviceToken: 'winToken'
},
{
deviceType: 'android',
deviceToken: undefined
}
];
var data = {};
parsePushAdapter.send(data, installations);
// Check android sender
expect(androidSender.send).toHaveBeenCalled();
var args = androidSender.send.calls.first().args;
expect(args[0]).toEqual(data);
expect(args[1]).toEqual([
makeDevice('androidToken')
]);
// Check ios sender
expect(iosSender.send).toHaveBeenCalled();
args = iosSender.send.calls.first().args;
expect(args[0]).toEqual(data);
expect(args[1]).toEqual([
makeDevice('iosToken')
]);
done();
});
function makeDevice(deviceToken, appIdentifier) {
return {
deviceToken: deviceToken,
appIdentifier: appIdentifier
};
}
});
+444
View File
@@ -2,6 +2,9 @@
// hungry/js/test/parse_query_test.js
//
// Some new tests are added.
'use strict';
const Parse = require('parse/node');
describe('Parse.Query testing', () => {
it("basic query", function(done) {
@@ -20,6 +23,105 @@ describe('Parse.Query testing', () => {
});
});
it("notEqualTo with Relation is working", function(done) {
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
var user1 = new Parse.User();
user1.setPassword("asdf");
user1.setUsername("qwerty");
var user2 = new Parse.User();
user2.setPassword("asdf");
user2.setUsername("asdf");
var Cake = Parse.Object.extend("Cake");
var cake1 = new Cake();
var cake2 = new Cake();
var cake3 = new Cake();
user.signUp().then(function(){
return user1.signUp();
}).then(function(){
return user2.signUp();
}).then(function(){
var relLike1 = cake1.relation("liker");
relLike1.add([user, user1]);
var relDislike1 = cake1.relation("hater");
relDislike1.add(user2);
return cake1.save();
}).then(function(){
var rellike2 = cake2.relation("liker");
rellike2.add([user, user1]);
var relDislike2 = cake2.relation("hater");
relDislike2.add(user2);
return cake2.save();
}).then(function(){
var rellike3 = cake3.relation("liker");
rellike3.add(user);
var relDislike3 = cake3.relation("hater");
relDislike3.add([user1, user2]);
return cake3.save();
}).then(function(){
var query = new Parse.Query(Cake);
// User2 likes nothing so we should receive 0
query.equalTo("liker", user2);
return query.find().then(function(results){
equal(results.length, 0);
});
}).then(function(){
var query = new Parse.Query(Cake);
// User1 likes two of three cakes
query.equalTo("liker", user1);
return query.find().then(function(results){
// It should return 2 -> cake 1 and cake 2
equal(results.length, 2);
});
}).then(function(){
var query = new Parse.Query(Cake);
// We want to know which cake the user1 is not appreciating -> cake3
query.notEqualTo("liker", user1);
return query.find().then(function(results){
// Should return 1 -> the cake 3
equal(results.length, 1);
});
}).then(function(){
var query = new Parse.Query(Cake);
// User2 is a hater of everything so we should receive 0
query.notEqualTo("hater", user2);
return query.find().then(function(results){
equal(results.length, 0);
});
}).then(function(){
var query = new Parse.Query(Cake);
// Only cake3 is liked by user
query.notContainedIn("liker", [user1]);
return query.find().then(function(results){
equal(results.length, 1);
});
}).then(function(){
var query = new Parse.Query(Cake);
// All the users
query.containedIn("liker", [user, user1, user2]);
// Exclude user 1
query.notEqualTo("liker", user1);
// Only cake3 is liked only by user1
return query.find().then(function(results){
equal(results.length, 1);
let cake = results[0];
expect(cake.id).toBe(cake3.id);
});
}).then(function(){
done();
})
});
it("query with limit", function(done) {
var baz = new TestObject({ foo: 'baz' });
var qux = new TestObject({ foo: 'qux' });
@@ -1419,6 +1521,153 @@ describe('Parse.Query testing', () => {
});
});
it('properly includes array', (done) => {
let objects = [];
let total = 0;
while(objects.length != 5) {
let object = new Parse.Object('AnObject');
object.set('key', objects.length);
total += objects.length;
objects.push(object);
}
Parse.Object.saveAll(objects).then(() => {
let object = new Parse.Object("AContainer");
object.set('objects', objects);
return object.save();
}).then(() => {
let query = new Parse.Query('AContainer');
query.include('objects');
return query.find()
}).then((results) => {
expect(results.length).toBe(1);
let res = results[0];
let objects = res.get('objects');
expect(objects.length).toBe(5);
objects.forEach((object) => {
total -= object.get('key');
});
expect(total).toBe(0);
done()
}, () => {
fail('should not fail');
done();
})
});
it('properly includes array of mixed objects', (done) => {
let objects = [];
let total = 0;
while(objects.length != 5) {
let object = new Parse.Object('AnObject');
object.set('key', objects.length);
total += objects.length;
objects.push(object);
}
while(objects.length != 10) {
let object = new Parse.Object('AnotherObject');
object.set('key', objects.length);
total += objects.length;
objects.push(object);
}
Parse.Object.saveAll(objects).then(() => {
let object = new Parse.Object("AContainer");
object.set('objects', objects);
return object.save();
}).then(() => {
let query = new Parse.Query('AContainer');
query.include('objects');
return query.find()
}).then((results) => {
expect(results.length).toBe(1);
let res = results[0];
let objects = res.get('objects');
expect(objects.length).toBe(10);
objects.forEach((object) => {
total -= object.get('key');
});
expect(total).toBe(0);
done()
}, (err) => {
fail('should not fail');
done();
})
});
it('properly nested array of mixed objects with bad ids', (done) => {
let objects = [];
let total = 0;
while(objects.length != 5) {
let object = new Parse.Object('AnObject');
object.set('key', objects.length);
objects.push(object);
}
while(objects.length != 10) {
let object = new Parse.Object('AnotherObject');
object.set('key', objects.length);
objects.push(object);
}
Parse.Object.saveAll(objects).then(() => {
let object = new Parse.Object("AContainer");
for (var i=0; i<objects.length; i++) {
if (i%2 == 0) {
objects[i].id = 'randomThing'
} else {
total += objects[i].get('key');
}
}
object.set('objects', objects);
return object.save();
}).then(() => {
let query = new Parse.Query('AContainer');
query.include('objects');
return query.find()
}).then((results) => {
expect(results.length).toBe(1);
let res = results[0];
let objects = res.get('objects');
expect(objects.length).toBe(5);
objects.forEach((object) => {
total -= object.get('key');
});
expect(total).toBe(0);
done()
}, (err) => {
console.error(err);
fail('should not fail');
done();
})
});
it('properly fetches nested pointers', (done) =>  {
let color = new Parse.Object('Color');
color.set('hex','#133733');
let circle = new Parse.Object('Circle');
circle.set('radius', 1337);
Parse.Object.saveAll([color, circle]).then(() => {
circle.set('color', color);
let badCircle = new Parse.Object('Circle');
badCircle.id = 'badId';
let complexFigure = new Parse.Object('ComplexFigure');
complexFigure.set('consistsOf', [circle, badCircle]);
return complexFigure.save();
}).then(() => {
let q = new Parse.Query('ComplexFigure');
q.include('consistsOf.color');
return q.find()
}).then((results) => {
expect(results.length).toBe(1);
let figure = results[0];
expect(figure.get('consistsOf').length).toBe(1);
expect(figure.get('consistsOf')[0].get('color').get('hex')).toBe('#133733');
done();
}, (err) => {
fail('should not fail');
done();
})
});
it("result object creation uses current extension", function(done) {
var ParentObject = Parse.Object.extend({ className: "ParentObject" });
// Add a foo() method to ChildObject.
@@ -1574,6 +1823,29 @@ describe('Parse.Query testing', () => {
});
});
it("dontSelect query without conditions", function(done) {
const RestaurantObject = Parse.Object.extend("Restaurant");
const PersonObject = Parse.Object.extend("Person");
const objects = [
new RestaurantObject({ location: "Djibouti" }),
new RestaurantObject({ location: "Ouagadougou" }),
new PersonObject({ name: "Bob", hometown: "Djibouti" }),
new PersonObject({ name: "Tom", hometown: "Yoloblahblahblah" }),
new PersonObject({ name: "Billy", hometown: "Ouagadougou" })
];
Parse.Object.saveAll(objects, function() {
const query = new Parse.Query(RestaurantObject);
const mainQuery = new Parse.Query(PersonObject);
mainQuery.doesNotMatchKeyInQuery("hometown", "location", query);
mainQuery.find().then(results => {
equal(results.length, 1);
equal(results[0].get('name'), 'Tom');
done();
});
});
});
it("object with length", function(done) {
var TestObject = Parse.Object.extend("TestObject");
var obj = new TestObject();
@@ -2088,4 +2360,176 @@ describe('Parse.Query testing', () => {
console.log(error);
});
});
// #371
it('should properly interpret a query', (done) => {
var query = new Parse.Query("C1");
var auxQuery = new Parse.Query("C1");
query.matchesKeyInQuery("A1", "A2", auxQuery);
query.include("A3");
query.include("A2");
query.find().then((result) => {
done();
}, (err) => {
console.error(err);
fail("should not failt");
done();
})
});
it('should properly interpret a query', (done) => {
var user = new Parse.User();
user.set("username", "foo");
user.set("password", "bar");
return user.save().then( (user) => {
var objIdQuery = new Parse.Query("_User").equalTo("objectId", user.id);
var blockedUserQuery = user.relation("blockedUsers").query();
var aResponseQuery = new Parse.Query("MatchRelationshipActivityResponse");
aResponseQuery.equalTo("userA", user);
aResponseQuery.equalTo("userAResponse", 1);
var bResponseQuery = new Parse.Query("MatchRelationshipActivityResponse");
bResponseQuery.equalTo("userB", user);
bResponseQuery.equalTo("userBResponse", 1);
var matchOr = Parse.Query.or(aResponseQuery, bResponseQuery);
var matchRelationshipA = new Parse.Query("_User");
matchRelationshipA.matchesKeyInQuery("objectId", "userAObjectId", matchOr);
var matchRelationshipB = new Parse.Query("_User");
matchRelationshipB.matchesKeyInQuery("objectId", "userBObjectId", matchOr);
var orQuery = Parse.Query.or(objIdQuery, blockedUserQuery, matchRelationshipA, matchRelationshipB);
var query = new Parse.Query("_User");
query.doesNotMatchQuery("objectId", orQuery);
return query.find();
}).then((res) => {
done();
done();
}, (err) => {
console.error(err);
fail("should not fail");
done();
});
});
it('should find objects with array of pointers', (done) => {
var objects = [];
while(objects.length != 5) {
var object = new Parse.Object('ContainedObject');
object.set('index', objects.length);
objects.push(object);
}
Parse.Object.saveAll(objects).then((objects) => {
var container = new Parse.Object('Container');
var pointers = objects.map((obj) => {
return {
__type: 'Pointer',
className: 'ContainedObject',
objectId: obj.id
}
})
container.set('objects', pointers);
let container2 = new Parse.Object('Container');
container2.set('objects', pointers.slice(2, 3));
return Parse.Object.saveAll([container, container2]);
}).then(() => {
let inQuery = new Parse.Query('ContainedObject');
inQuery.greaterThanOrEqualTo('index', 1);
let query = new Parse.Query('Container');
query.matchesQuery('objects', inQuery);
return query.find();
}).then((results) => {
if (results) {
expect(results.length).toBe(2);
}
done();
}).fail((err) => {
console.error(err);
fail('should not fail');
done();
})
})
it('query with two OR subqueries (regression test #1259)', done => {
let relatedObject = new Parse.Object('Class2');
relatedObject.save().then(relatedObject => {
let anObject = new Parse.Object('Class1');
let relation = anObject.relation('relation');
relation.add(relatedObject);
return anObject.save();
}).then(anObject => {
let q1 = anObject.relation('relation').query();
q1.doesNotExist('nonExistantKey1');
let q2 = anObject.relation('relation').query();
q2.doesNotExist('nonExistantKey2');
let orQuery = Parse.Query.or(q1, q2).find().then(results => {
expect(results.length).toEqual(1);
expect(results[0].objectId).toEqual(q1.objectId);
done();
});
});
});
it('objectId containedIn with multiple large array', done => {
let obj = new Parse.Object('MyClass');
obj.save().then(obj => {
let longListOfStrings = [];
for (let i = 0; i < 130; i++) {
longListOfStrings.push(i.toString());
}
longListOfStrings.push(obj.id);
let q = new Parse.Query('MyClass');
q.containedIn('objectId', longListOfStrings);
q.containedIn('objectId', longListOfStrings);
return q.find();
}).then(results => {
expect(results.length).toEqual(1);
done();
});
});
it('include for specific object', function(done){
var child = new Parse.Object('Child');
var parent = new Parse.Object('Parent');
child.set('foo', 'bar');
parent.set('child', child);
Parse.Object.saveAll([child, parent], function(response){
var savedParent = response[1];
var parentQuery = new Parse.Query('Parent');
parentQuery.include('child');
parentQuery.get(savedParent.id, {
success: function(parentObj) {
var childPointer = parentObj.get('child');
ok(childPointer);
equal(childPointer.get('foo'), 'bar');
done();
}
});
});
});
it('select keys for specific object', function(done){
var Foobar = new Parse.Object('Foobar');
Foobar.set('foo', 'bar');
Foobar.set('fizz', 'buzz');
Foobar.save({
success: function(savedFoobar){
var foobarQuery = new Parse.Query('Foobar');
foobarQuery.select('fizz');
foobarQuery.get(savedFoobar.id,{
success: function(foobarObj){
equal(foobarObj.get('fizz'), 'buzz');
equal(foobarObj.get('foo'), undefined);
done();
}
});
}
})
});
});
+349 -1
View File
@@ -1,3 +1,4 @@
'use strict';
// This is a port of the test suite:
// hungry/js/test/parse_relation_test.js
@@ -247,6 +248,177 @@ describe('Parse.Relation testing', () => {
});
});
it("queries on relation fields with multiple containedIn (regression test for #1271)", (done) => {
let ChildObject = Parse.Object.extend("ChildObject");
let childObjects = [];
for (let i = 0; i < 10; i++) {
childObjects.push(new ChildObject({x: i}));
}
Parse.Object.saveAll(childObjects).then(() => {
let ParentObject = Parse.Object.extend("ParentObject");
let parent = new ParentObject();
parent.set("x", 4);
let parent1Children = parent.relation("child");
parent1Children.add(childObjects[0]);
parent1Children.add(childObjects[1]);
parent1Children.add(childObjects[2]);
let parent2 = new ParentObject();
parent2.set("x", 3);
let parent2Children = parent2.relation("child");
parent2Children.add(childObjects[4]);
parent2Children.add(childObjects[5]);
parent2Children.add(childObjects[6]);
let parent2OtherChildren = parent2.relation("otherChild");
parent2OtherChildren.add(childObjects[0]);
parent2OtherChildren.add(childObjects[1]);
parent2OtherChildren.add(childObjects[2]);
return Parse.Object.saveAll([parent, parent2]);
}).then(() => {
let objectsWithChild0InBothChildren = new Parse.Query(ParentObject);
objectsWithChild0InBothChildren.containedIn("child", [childObjects[0]]);
objectsWithChild0InBothChildren.containedIn("otherChild", [childObjects[0]]);
return objectsWithChild0InBothChildren.find();
}).then(objectsWithChild0InBothChildren => {
//No parent has child 0 in both it's "child" and "otherChild" field;
expect(objectsWithChild0InBothChildren.length).toEqual(0);
}).then(() => {
let objectsWithChild4andOtherChild1 = new Parse.Query(ParentObject);
objectsWithChild4andOtherChild1.containedIn("child", [childObjects[4]]);
objectsWithChild4andOtherChild1.containedIn("otherChild", [childObjects[1]]);
return objectsWithChild4andOtherChild1.find();
}).then(objects => {
// parent2 has child 4 and otherChild 1
expect(objects.length).toEqual(1);
done();
});
});
it("query on pointer and relation fields with equal", (done) => {
var ChildObject = Parse.Object.extend("ChildObject");
var childObjects = [];
for (var i = 0; i < 10; i++) {
childObjects.push(new ChildObject({x: i}));
}
Parse.Object.saveAll(childObjects).then(() => {
var ParentObject = Parse.Object.extend("ParentObject");
var parent = new ParentObject();
parent.set("x", 4);
var relation = parent.relation("toChilds");
relation.add(childObjects[0]);
relation.add(childObjects[1]);
relation.add(childObjects[2]);
var parent2 = new ParentObject();
parent2.set("x", 3);
parent2.set("toChild", childObjects[2]);
var parents = [];
parents.push(parent);
parents.push(parent2);
parents.push(new ParentObject());
return Parse.Object.saveAll(parents).then(() => {
var query = new Parse.Query(ParentObject);
query.equalTo("objectId", parent.id);
query.equalTo("toChilds", childObjects[2]);
return query.find().then((list) => {
equal(list.length, 1, "There should be 1 result");
done();
});
});
});
});
it("query on pointer and relation fields with equal bis", (done) => {
var ChildObject = Parse.Object.extend("ChildObject");
var childObjects = [];
for (var i = 0; i < 10; i++) {
childObjects.push(new ChildObject({x: i}));
}
Parse.Object.saveAll(childObjects).then(() => {
var ParentObject = Parse.Object.extend("ParentObject");
var parent = new ParentObject();
parent.set("x", 4);
var relation = parent.relation("toChilds");
relation.add(childObjects[0]);
relation.add(childObjects[1]);
relation.add(childObjects[2]);
var parent2 = new ParentObject();
parent2.set("x", 3);
parent2.relation("toChilds").add(childObjects[2]);
var parents = [];
parents.push(parent);
parents.push(parent2);
parents.push(new ParentObject());
return Parse.Object.saveAll(parents).then(() => {
var query = new Parse.Query(ParentObject);
query.equalTo("objectId", parent2.id);
// childObjects[2] is in 2 relations
// before the fix, that woul yield 2 results
query.equalTo("toChilds", childObjects[2]);
return query.find().then((list) => {
equal(list.length, 1, "There should be 1 result");
done();
});
});
});
});
it("or queries on pointer and relation fields", (done) => {
var ChildObject = Parse.Object.extend("ChildObject");
var childObjects = [];
for (var i = 0; i < 10; i++) {
childObjects.push(new ChildObject({x: i}));
}
Parse.Object.saveAll(childObjects).then(() => {
var ParentObject = Parse.Object.extend("ParentObject");
var parent = new ParentObject();
parent.set("x", 4);
var relation = parent.relation("toChilds");
relation.add(childObjects[0]);
relation.add(childObjects[1]);
relation.add(childObjects[2]);
var parent2 = new ParentObject();
parent2.set("x", 3);
parent2.set("toChild", childObjects[2]);
var parents = [];
parents.push(parent);
parents.push(parent2);
parents.push(new ParentObject());
return Parse.Object.saveAll(parents).then(() => {
var query1 = new Parse.Query(ParentObject);
query1.containedIn("toChilds", [childObjects[2]]);
var query2 = new Parse.Query(ParentObject);
query2.equalTo("toChild", childObjects[2]);
var query = Parse.Query.or(query1, query2);
return query.find().then((list) => {
var objectIds = list.map(function(item){
return item.id;
});
expect(objectIds.indexOf(parent.id)).not.toBe(-1);
expect(objectIds.indexOf(parent2.id)).not.toBe(-1);
equal(list.length, 2, "There should be 2 results");
done();
});
});
});
});
it("Get query on relation using un-fetched parent object", (done) => {
// Setup data model
var Wheel = Parse.Object.extend('Wheel');
@@ -334,5 +506,181 @@ describe('Parse.Relation testing', () => {
});
});
});
it('should properly get related objects with unfetched queries', (done) => {
let objects = [];
let owners = [];
let allObjects = [];
// Build 10 Objects and 10 owners
while (objects.length != 10) {
let object = new Parse.Object('AnObject');
object.set({
index: objects.length,
even: objects.length % 2 == 0
});
objects.push(object);
let owner = new Parse.Object('AnOwner');
owners.push(owner);
allObjects.push(object);
allObjects.push(owner);
}
let anotherOwner = new Parse.Object('AnotherOwner');
return Parse.Object.saveAll(allObjects.concat([anotherOwner])).then(() => {
// put all the AnObject into the anotherOwner relationKey
anotherOwner.relation('relationKey').add(objects);
// Set each object[i] into owner[i];
owners.forEach((owner,i) => {
owner.set('key', objects[i]);
});
return Parse.Object.saveAll(owners.concat([anotherOwner]));
}).then(() => {
// Query on the relation of another owner
let object = new Parse.Object('AnotherOwner');
object.id = anotherOwner.id;
let relationQuery = object.relation('relationKey').query();
// Just get the even ones
relationQuery.equalTo('even', true);
// Make the query on anOwner
let query = new Parse.Query('AnOwner');
// where key match the relation query.
query.matchesQuery('key', relationQuery);
query.include('key');
return query.find();
}).then((results) => {
expect(results.length).toBe(5);
results.forEach((result) => {
expect(result.get('key').get('even')).toBe(true);
});
return Promise.resolve();
}).then(() => {
// Query on the relation of another owner
let object = new Parse.Object('AnotherOwner');
object.id = anotherOwner.id;
let relationQuery = object.relation('relationKey').query();
// Just get the even ones
relationQuery.equalTo('even', true);
// Make the query on anOwner
let query = new Parse.Query('AnOwner');
// where key match the relation query.
query.doesNotMatchQuery('key', relationQuery);
query.include('key');
return query.find();
}).then((results) => {
expect(results.length).toBe(5);
results.forEach((result) => {
expect(result.get('key').get('even')).toBe(false);
});
done();
})
});
it("select query", function(done) {
var RestaurantObject = Parse.Object.extend("Restaurant");
var PersonObject = Parse.Object.extend("Person");
var OwnerObject = Parse.Object.extend('Owner');
var restaurants = [
new RestaurantObject({ ratings: 5, location: "Djibouti" }),
new RestaurantObject({ ratings: 3, location: "Ouagadougou" }),
];
let persons = [
new PersonObject({ name: "Bob", hometown: "Djibouti" }),
new PersonObject({ name: "Tom", hometown: "Ouagadougou" }),
new PersonObject({ name: "Billy", hometown: "Detroit" }),
];
let owner = new OwnerObject({name: 'Joe'});
let ownerId;
let allObjects = [owner].concat(restaurants).concat(persons);
expect(allObjects.length).toEqual(6);
Parse.Object.saveAll([owner].concat(restaurants).concat(persons)).then(function() {
ownerId = owner.id;
owner.relation('restaurants').add(restaurants);
return owner.save()
}).then(() => {
let unfetchedOwner = new OwnerObject();
unfetchedOwner.id = owner.id;
var query = unfetchedOwner.relation('restaurants').query();
query.greaterThan("ratings", 4);
var mainQuery = new Parse.Query(PersonObject);
mainQuery.matchesKeyInQuery("hometown", "location", query);
mainQuery.find(expectSuccess({
success: function(results) {
equal(results.length, 1);
if (results.length > 0) {
equal(results[0].get('name'), 'Bob');
}
done();
}
}));
});
});
it("dontSelect query", function(done) {
var RestaurantObject = Parse.Object.extend("Restaurant");
var PersonObject = Parse.Object.extend("Person");
var OwnerObject = Parse.Object.extend('Owner');
var restaurants = [
new RestaurantObject({ ratings: 5, location: "Djibouti" }),
new RestaurantObject({ ratings: 3, location: "Ouagadougou" }),
];
let persons = [
new PersonObject({ name: "Bob", hometown: "Djibouti" }),
new PersonObject({ name: "Tom", hometown: "Ouagadougou" }),
new PersonObject({ name: "Billy", hometown: "Detroit" }),
];
let owner = new OwnerObject({name: 'Joe'});
let ownerId;
let allObjects = [owner].concat(restaurants).concat(persons);
expect(allObjects.length).toEqual(6);
Parse.Object.saveAll([owner].concat(restaurants).concat(persons)).then(function() {
ownerId = owner.id;
owner.relation('restaurants').add(restaurants);
return owner.save()
}).then(() => {
let unfetchedOwner = new OwnerObject();
unfetchedOwner.id = owner.id;
var query = unfetchedOwner.relation('restaurants').query();
query.greaterThan("ratings", 4);
var mainQuery = new Parse.Query(PersonObject);
mainQuery.doesNotMatchKeyInQuery("hometown", "location", query);
mainQuery.ascending('name');
mainQuery.find(expectSuccess({
success: function(results) {
equal(results.length, 2);
if (results.length > 0) {
equal(results[0].get('name'), 'Billy');
equal(results[1].get('name'), 'Tom');
}
done();
}
}));
});
});
it('relations are not bidirectional (regression test for #871)', done => {
let PersonObject = Parse.Object.extend("Person");
let p1 = new PersonObject();
let p2 = new PersonObject();
Parse.Object.saveAll([p1, p2]).then(results => {
let p1 = results[0];
let p2 = results[1];
let relation = p1.relation('relation');
relation.add(p2);
p1.save().then(() => {
let query = new Parse.Query(PersonObject);
query.equalTo('relation', p1);
query.find().then(results => {
expect(results.length).toEqual(0);
let query = new Parse.Query(PersonObject);
query.equalTo('relation', p2);
query.find().then(results => {
expect(results.length).toEqual(1);
expect(results[0].objectId).toEqual(p1.objectId);
done();
});
});
})
});
});
});
+287 -2
View File
@@ -1,7 +1,10 @@
"use strict";
// Roles are not accessible without the master key, so they are not intended
// for use by clients. We can manually test them using the master key.
var RestQuery = require("../src/RestQuery");
var Auth = require("../src/Auth").Auth;
var Config = require("../src/Config");
describe('Parse Role testing', () => {
@@ -49,7 +52,7 @@ describe('Parse Role testing', () => {
}).then((x) => {
x.set('foo', 'baz');
// This should fail:
return x.save();
return x.save({},{sessionToken: ""});
}).then((x) => {
fail('Should not have been able to save.');
}, (e) => {
@@ -58,5 +61,287 @@ describe('Parse Role testing', () => {
});
var createRole = function(name, sibling, user) {
var role = new Parse.Role(name, new Parse.ACL());
if (user) {
var users = role.relation('users');
users.add(user);
}
if (sibling) {
role.relation('roles').add(sibling);
}
return role.save({}, { useMasterKey: true });
};
it("should not recursively load the same role multiple times", (done) => {
var rootRole = "RootRole";
var roleNames = ["FooRole", "BarRole", "BazRole"];
var allRoles = [rootRole].concat(roleNames);
var roleObjs = {};
var createAllRoles = function(user) {
var promises = allRoles.map(function(roleName) {
return createRole(roleName, null, user)
.then(function(roleObj) {
roleObjs[roleName] = roleObj;
return roleObj;
});
});
return Promise.all(promises);
};
var restExecute = spyOn(RestQuery.prototype, "execute").and.callThrough();
var user,
auth,
getAllRolesSpy;
createTestUser().then( (newUser) => {
user = newUser;
return createAllRoles(user);
}).then ( (roles) => {
var rootRoleObj = roleObjs[rootRole];
roles.forEach(function(role, i) {
// Add all roles to the RootRole
if (role.id !== rootRoleObj.id) {
role.relation("roles").add(rootRoleObj);
}
// Add all "roleNames" roles to the previous role
if (i > 0) {
role.relation("roles").add(roles[i - 1]);
}
});
return Parse.Object.saveAll(roles, { useMasterKey: true });
}).then( () => {
auth = new Auth({config: new Config("test"), isMaster: true, user: user});
getAllRolesSpy = spyOn(auth, "_getAllRoleNamesForId").and.callThrough();
return auth._loadRoles();
}).then ( (roles) => {
expect(roles.length).toEqual(4);
allRoles.forEach(function(name) {
expect(roles.indexOf("role:"+name)).not.toBe(-1);
});
// 1 Query for the initial setup
// 4 Queries for all the specific roles
// 1 Query for the final $in
expect(restExecute.calls.count()).toEqual(6);
// 4 One query for each of the roles
// 3 Queries for the "RootRole"
expect(getAllRolesSpy.calls.count()).toEqual(7);
done()
}).catch( () => {
fail("should succeed");
done();
});
});
it("should recursively load roles", (done) => {
var rolesNames = ["FooRole", "BarRole", "BazRole"];
var roleIds = {};
createTestUser().then( (user) => {
// Put the user on the 1st role
return createRole(rolesNames[0], null, user).then( (aRole) => {
roleIds[aRole.get("name")] = aRole.id;
// set the 1st role as a sibling of the second
// user will should have 2 role now
return createRole(rolesNames[1], aRole, null);
}).then( (anotherRole) => {
roleIds[anotherRole.get("name")] = anotherRole.id;
// set this role as a sibling of the last
// the user should now have 3 roles
return createRole(rolesNames[2], anotherRole, null);
}).then( (lastRole) => {
roleIds[lastRole.get("name")] = lastRole.id;
var auth = new Auth({ config: new Config("test"), isMaster: true, user: user });
return auth._loadRoles();
})
}).then( (roles) => {
expect(roles.length).toEqual(3);
rolesNames.forEach( (name) => {
expect(roles.indexOf('role:'+name)).not.toBe(-1);
});
done();
}, function(err){
fail("should succeed")
done();
});
});
it("_Role object should not save without name.", (done) => {
var role = new Parse.Role();
role.save(null,{useMasterKey:true})
.then((r) => {
fail("_Role object should not save without name.");
}, (error) => {
expect(error.code).toEqual(111);
role.set('name','testRole');
role.save(null,{useMasterKey:true})
.then((r2)=>{
fail("_Role object should not save without ACL.");
}, (error2) =>{
expect(error2.code).toEqual(111);
done();
});
});
});
it("Should properly resolve roles", (done) => {
let admin = new Parse.Role("Admin", new Parse.ACL());
let moderator = new Parse.Role("Moderator", new Parse.ACL());
let superModerator = new Parse.Role("SuperModerator", new Parse.ACL());
let contentManager = new Parse.Role('ContentManager', new Parse.ACL());
let superContentManager = new Parse.Role('SuperContentManager', new Parse.ACL());
Parse.Object.saveAll([admin, moderator, contentManager, superModerator, superContentManager], {useMasterKey: true}).then(() => {
contentManager.getRoles().add([moderator, superContentManager]);
moderator.getRoles().add([admin, superModerator]);
superContentManager.getRoles().add(superModerator);
return Parse.Object.saveAll([admin, moderator, contentManager, superModerator, superContentManager], {useMasterKey: true});
}).then(() => {
var auth = new Auth({ config: new Config("test"), isMaster: true });
// For each role, fetch their sibling, what they inherit
// return with result and roleId for later comparison
let promises = [admin, moderator, contentManager, superModerator].map((role) => {
return auth._getAllRoleNamesForId(role.id).then((result) => {
return Parse.Promise.as({
id: role.id,
name: role.get('name'),
roleIds: result
});
})
});
return Parse.Promise.when(promises);
}).then((results) => {
results.forEach((result) => {
let id = result.id;
let roleIds = result.roleIds;
if (id == admin.id) {
expect(roleIds.length).toBe(2);
expect(roleIds.indexOf(moderator.id)).not.toBe(-1);
expect(roleIds.indexOf(contentManager.id)).not.toBe(-1);
} else if (id == moderator.id) {
expect(roleIds.length).toBe(1);
expect(roleIds.indexOf(contentManager.id)).toBe(0);
} else if (id == contentManager.id) {
expect(roleIds.length).toBe(0);
} else if (id == superModerator.id) {
expect(roleIds.length).toBe(3);
expect(roleIds.indexOf(moderator.id)).not.toBe(-1);
expect(roleIds.indexOf(contentManager.id)).not.toBe(-1);
expect(roleIds.indexOf(superContentManager.id)).not.toBe(-1);
}
});
done();
}).fail((err) => {
console.error(err);
done();
})
});
it('can create role and query empty users', (done)=> {
var roleACL = new Parse.ACL();
roleACL.setPublicReadAccess(true);
var role = new Parse.Role('subscribers', roleACL);
role.save({}, {useMasterKey : true})
.then((x)=>{
var query = role.relation('users').query();
query.find({useMasterKey : true})
.then((users)=>{
done();
}, (e)=>{
fail('should not have errors');
done();
});
}, (e) => {
console.log(e);
fail('should not have errored');
});
});
// Based on various scenarios described in issues #827 and #683,
it('should properly handle role permissions on objects', (done) => {
var user, user2, user3;
var role, role2, role3;
var obj, obj2;
var prACL = new Parse.ACL();
prACL.setPublicReadAccess(true);
var adminACL, superACL, customerACL;
createTestUser().then((x) => {
user = x;
user2 = new Parse.User();
return user2.save({ username: 'user2', password: 'omgbbq' });
}).then((x) => {
user3 = new Parse.User();
return user3.save({ username: 'user3', password: 'omgbbq' });
}).then((x) => {
role = new Parse.Role('Admin', prACL);
role.getUsers().add(user);
return role.save({}, { useMasterKey: true });
}).then(() => {
adminACL = new Parse.ACL();
adminACL.setRoleReadAccess("Admin", true);
adminACL.setRoleWriteAccess("Admin", true);
role2 = new Parse.Role('Super', prACL);
role2.getUsers().add(user2);
return role2.save({}, { useMasterKey: true });
}).then(() => {
superACL = new Parse.ACL();
superACL.setRoleReadAccess("Super", true);
superACL.setRoleWriteAccess("Super", true);
role.getRoles().add(role2);
return role.save({}, { useMasterKey: true });
}).then(() => {
role3 = new Parse.Role('Customer', prACL);
role3.getUsers().add(user3);
role3.getRoles().add(role);
return role3.save({}, { useMasterKey: true });
}).then(() => {
customerACL = new Parse.ACL();
customerACL.setRoleReadAccess("Customer", true);
customerACL.setRoleWriteAccess("Customer", true);
var query = new Parse.Query('_Role');
return query.find({ useMasterKey: true });
}).then((x) => {
expect(x.length).toEqual(3);
obj = new Parse.Object('TestObjectRoles');
obj.set('ACL', customerACL);
return obj.save(null, { useMasterKey: true });
}).then(() => {
// Above, the Admin role was added to the Customer role.
// An object secured by the Customer ACL should be able to be edited by the Admin user.
obj.set('changedByAdmin', true);
return obj.save(null, { sessionToken: user.getSessionToken() });
}).then(() => {
obj2 = new Parse.Object('TestObjectRoles');
obj2.set('ACL', adminACL);
return obj2.save(null, { useMasterKey: true });
}, (e) => {
fail('Admin user should have been able to save.');
done();
}).then(() => {
// An object secured by the Admin ACL should not be able to be edited by a Customer role user.
obj2.set('changedByCustomer', true);
return obj2.save(null, { sessionToken: user3.getSessionToken() });
}).then(() => {
fail('Customer user should not have been able to save.');
done();
}, (e) => {
expect(e.code).toEqual(101);
done();
})
});
});
+534 -6
View File
@@ -5,8 +5,11 @@
// Tests that involve revocable sessions.
// Tests that involve sending password reset emails.
"use strict";
var request = require('request');
var passwordCrypto = require('../src/password');
var Config = require('../src/Config');
function verifyACL(user) {
const ACL = user.getACL();
@@ -23,6 +26,7 @@ function verifyACL(user) {
}
describe('Parse.User testing', () => {
it("user sign up class method", (done) => {
Parse.User.signUp("asdf", "zxcv", null, {
success: function(user) {
@@ -52,6 +56,11 @@ describe('Parse.User testing', () => {
success: function(user) {
Parse.User.logIn("non_existent_user", "asdf3",
expectError(Parse.Error.OBJECT_NOT_FOUND, done));
},
error: function(err) {
console.error(err);
fail("Shit should not fail");
done();
}
});
});
@@ -80,8 +89,6 @@ describe('Parse.User testing', () => {
});
it("user login with files", (done) => {
"use strict";
let file = new Parse.File("yolo.txt", [1,2,3], "text/plain");
file.save().then((file) => {
return Parse.User.signUp("asdf", "zxcv", { "file" : file });
@@ -95,6 +102,33 @@ describe('Parse.User testing', () => {
});
});
describe('become', () => {
it('sends token back', done => {
let user = null;
var sessionToken = null;
Parse.User.signUp('Jason', 'Parse', { 'code': 'red' }).then(newUser => {
user = newUser;
expect(user.get('code'), 'red');
sessionToken = newUser.getSessionToken();
expect(sessionToken).toBeDefined();
return Parse.User.become(sessionToken);
}).then(newUser => {
expect(newUser.id).toEqual(user.id);
expect(newUser.get('username'), 'Jason');
expect(newUser.get('code'), 'red');
expect(newUser.getSessionToken()).toEqual(sessionToken);
}).then(() => {
done();
}, error => {
fail(error);
done();
});
});
});
it("become", (done) => {
var user = null;
var sessionToken = null;
@@ -873,6 +907,50 @@ describe('Parse.User testing', () => {
};
};
var getMockMyOauthProvider = function() {
return {
authData: {
id: "12345",
access_token: "12345",
expiration_date: new Date().toJSON(),
},
shouldError: false,
loggedOut: false,
synchronizedUserId: null,
synchronizedAuthToken: null,
synchronizedExpiration: null,
authenticate: function(options) {
if (this.shouldError) {
options.error(this, "An error occurred");
} else if (this.shouldCancel) {
options.error(this, null);
} else {
options.success(this, this.authData);
}
},
restoreAuthentication: function(authData) {
if (!authData) {
this.synchronizedUserId = null;
this.synchronizedAuthToken = null;
this.synchronizedExpiration = null;
return true;
}
this.synchronizedUserId = authData.id;
this.synchronizedAuthToken = authData.access_token;
this.synchronizedExpiration = authData.expiration_date;
return true;
},
getAuthType: function() {
return "myoauth";
},
deauthenticate: function() {
this.loggedOut = true;
this.restoreAuthentication(null);
}
};
};
var ExtendedUser = Parse.User.extend({
extended: function() {
return true;
@@ -901,6 +979,29 @@ describe('Parse.User testing', () => {
});
});
it('log in with provider with files', done => {
let provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
let file = new Parse.File("yolo.txt", [1, 2, 3], "text/plain");
file.save().then(file => {
let user = new Parse.User();
user.set('file', file);
return user._linkWith('facebook', {});
}).then(user => {
expect(user._isLinked("facebook")).toBeTruthy();
return Parse.User._logInWith('facebook', {});
}).then(user => {
let fileAgain = user.get('file');
expect(fileAgain.name()).toMatch(/yolo.txt$/);
expect(fileAgain.url()).toMatch(/yolo.txt$/);
}).then(() => {
done();
}, error => {
fail(error);
done();
});
});
it("log in with provider twice", (done) => {
var provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
@@ -976,6 +1077,32 @@ describe('Parse.User testing', () => {
});
});
it("login with provider should not call beforeSave trigger", (done) => {
var provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("facebook", {
success: function(model) {
Parse.User.logOut();
Parse.Cloud.beforeSave(Parse.User, function(req, res) {
res.error("Before save shouldn't be called on login");
});
Parse.User._logInWith("facebook", {
success: function(innerModel) {
Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className);
done();
},
error: function(model, error) {
ok(undefined, error);
Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className);
done();
}
});
}
});
});
it("link with provider", (done) => {
var provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
@@ -1204,6 +1331,177 @@ describe('Parse.User testing', () => {
});
});
it("link multiple providers", (done) => {
var provider = getMockFacebookProvider();
var mockProvider = getMockMyOauthProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("facebook", {
success: function(model) {
ok(model instanceof Parse.User, "Model should be a Parse.User");
strictEqual(Parse.User.current(), model);
ok(model.extended(), "Should have used the subclass.");
strictEqual(provider.authData.id, provider.synchronizedUserId);
strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
ok(model._isLinked("facebook"), "User should be linked to facebook");
Parse.User._registerAuthenticationProvider(mockProvider);
let objectId = model.id;
model._linkWith("myoauth", {
success: function(model) {
expect(model.id).toEqual(objectId);
ok(model._isLinked("facebook"), "User should be linked to facebook");
ok(model._isLinked("myoauth"), "User should be linked to myoauth");
done();
},
error: function(error) {
console.error(error);
fail('SHould not fail');
done();
}
})
},
error: function(model, error) {
ok(false, "linking should have worked");
done();
}
});
});
it("link multiple providers and update token", (done) => {
var provider = getMockFacebookProvider();
var mockProvider = getMockMyOauthProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("facebook", {
success: function(model) {
ok(model instanceof Parse.User, "Model should be a Parse.User");
strictEqual(Parse.User.current(), model);
ok(model.extended(), "Should have used the subclass.");
strictEqual(provider.authData.id, provider.synchronizedUserId);
strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
ok(model._isLinked("facebook"), "User should be linked to facebook");
Parse.User._registerAuthenticationProvider(mockProvider);
let objectId = model.id;
model._linkWith("myoauth", {
success: function(model) {
expect(model.id).toEqual(objectId);
ok(model._isLinked("facebook"), "User should be linked to facebook");
ok(model._isLinked("myoauth"), "User should be linked to myoauth");
model._linkWith("facebook", {
success: () => {
ok(model._isLinked("facebook"), "User should be linked to facebook");
ok(model._isLinked("myoauth"), "User should be linked to myoauth");
done();
},
error: () => {
fail('should link again');
done();
}
})
},
error: function(error) {
console.error(error);
fail('SHould not fail');
done();
}
})
},
error: function(model, error) {
ok(false, "linking should have worked");
done();
}
});
});
it('should fail linking with existing', (done) => {
var provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("facebook", {
success: function(model) {
Parse.User.logOut().then(() => {
let user = new Parse.User();
user.setUsername('user');
user.setPassword('password');
return user.signUp().then(() => {
// try to link here
user._linkWith('facebook', {
success: () => {
fail('should not succeed');
done();
},
error: (err) => {
done();
}
});
});
});
}
});
});
it('should fail linking with existing', (done) => {
var provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("facebook", {
success: function(model) {
let userId = model.id;
Parse.User.logOut().then(() => {
request.post({
url:Parse.serverURL+'/classes/_User',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest'
},
json: {authData: {facebook: provider.authData}}
}, (err,res, body) => {
// make sure the location header is properly set
expect(userId).not.toBeUndefined();
expect(body.objectId).toEqual(userId);
expect(res.headers.location).toEqual(Parse.serverURL+'/users/'+userId);
done();
});
});
}
});
});
it('should have authData in beforeSave and afterSave', (done) => {
Parse.Cloud.beforeSave('_User', (request, response) => {
let authData = request.object.get('authData');
expect(authData).not.toBeUndefined();
if (authData) {
expect(authData.facebook.id).toEqual('8675309');
expect(authData.facebook.access_token).toEqual('jenny');
} else {
fail('authData should be set');
}
response.success();
});
Parse.Cloud.afterSave('_User', (request, response) => {
let authData = request.object.get('authData');
expect(authData).not.toBeUndefined();
if (authData) {
expect(authData.facebook.id).toEqual('8675309');
expect(authData.facebook.access_token).toEqual('jenny');
} else {
fail('authData should be set');
}
response.success();
});
var provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("facebook", {
success: function(model) {
Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className);
Parse.Cloud._removeHook('Triggers', 'afterSave', Parse.User.className);
done();
}
});
});
it('set password then change password', (done) => {
Parse.User.signUp('bob', 'barker').then((bob) => {
bob.setPassword('meower');
@@ -1471,9 +1769,37 @@ describe('Parse.User testing', () => {
});
});
it('user get session from token', (done) => {
it('user get session from token on signup', (done) => {
Parse.Promise.as().then(() => {
return Parse.User.signUp("finn", "human", { foo: "bar" });
}).then((user) => {
request.get({
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Session-Token': user.getSessionToken(),
'X-Parse-REST-API-Key': 'rest'
},
url: 'http://localhost:8378/1/sessions/me',
}, (error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(typeof b.sessionToken).toEqual('string');
expect(typeof b.createdWith).toEqual('object');
expect(b.createdWith.action).toEqual('signup');
expect(typeof b.user).toEqual('object');
expect(b.user.objectId).toEqual(user.id);
done();
});
});
});
it('user get session from token on login', (done) => {
Parse.Promise.as().then(() => {
return Parse.User.signUp("finn", "human", { foo: "bar" });
}).then((user) => {
return Parse.User.logOut().then(() => {
return Parse.User.logIn("finn", "human");
})
}).then((user) => {
request.get({
headers: {
@@ -1628,7 +1954,7 @@ describe('Parse.User testing', () => {
done();
});
});
it('test parse user become', (done) => {
var sessionToken = null;
Parse.Promise.as().then(function() {
@@ -1670,7 +1996,209 @@ describe('Parse.User testing', () => {
expect(e.code).toEqual(Parse.Error.SESSION_MISSING);
done();
});
})
});
it('support user/password signup with empty authData block', (done) => {
// The android SDK can send an empty authData object along with username and password.
Parse.User.signUp('artof', 'thedeal', { authData: {} }).then((user) => {
done();
}, (error) => {
fail('Signup should have succeeded.');
done();
});
});
it("session expiresAt correct format", (done) => {
Parse.User.signUp("asdf", "zxcv", null, {
success: function(user) {
request.get({
url: 'http://localhost:8378/1/classes/_Session',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
}, (error, response, body) => {
expect(body.results[0].expiresAt.__type).toEqual('Date');
done();
})
}
});
});
// Sometimes the authData still has null on that keys
// https://github.com/ParsePlatform/parse-server/issues/935
it('should cleanup null authData keys', (done) => {
let database = new Config(Parse.applicationId).database;
database.create('_User', {
username: 'user',
password: '$2a$10$8/wZJyEuiEaobBBqzTG.jeY.XSFJd0rzaN//ososvEI4yLqI.4aie',
_auth_data_facebook: null
}, {}).then(() => {
return new Promise((resolve, reject) => {
request.get({
url: 'http://localhost:8378/1/login?username=user&password=test',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
json: true
}, (err, res, body) => {
if (err) {
reject(err);
} else {
resolve(body);
}
})
})
}).then((user) => {
let authData = user.authData;
expect(user.username).toEqual('user');
expect(authData).toBeUndefined();
done();
}).catch((err) => {
fail('this should not fail');
done();
})
});
// https://github.com/ParsePlatform/parse-server/issues/1198
it('should cleanup null authData keys ParseUser update', (done) => {
Parse.Cloud.beforeSave('_User', (req, res) => {
req.object.set('foo', 'bar');
res.success();
});
// Simulate anonymous user save
new Promise((resolve, reject) => {
request.post({
url: 'http://localhost:8378/1/classes/_User',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
},
json: {authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}}
}, (err, res, body) => {
if (err) {
reject(err);
} else {
resolve(body);
}
});
}).then((user) => {
// Simulate registration
return new Promise((resolve, reject) => {
request.put({
url: 'http://localhost:8378/1/classes/_User/' + user.objectId,
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Session-Token': user.sessionToken,
'X-Parse-REST-API-Key': 'rest',
},
json: {
authData: {anonymous: null},
user: 'user',
password: 'password',
}
}, (err, res, body) => {
if (err) {
reject(err);
} else {
resolve(body);
}
});
});
}).then((user) => {
expect(typeof user).toEqual('object');
expect(user.authData).toBeUndefined();
Parse.Cloud._removeHook('Triggers', 'beforeSave', '_User');
done();
}).catch((err) => {
fail('no request should fail: ' + JSON.stringify(err));
Parse.Cloud._removeHook('Triggers', 'beforeSave', '_User');
done();
});
});
it('should aftersave with full object', (done) => {
var hit = 0;
Parse.Cloud.afterSave('_User', (req, res) => {
hit++;
expect(req.object.get('username')).toEqual('User');
res.success();
});
let user = new Parse.User()
user.setUsername('User');
user.setPassword('pass');
user.signUp().then(()=> {
user.set('hello', 'world');
return user.save();
}).then(() => {
Parse.Cloud._removeHook('Triggers', 'afterSave', '_User');
done();
});
});
it('changes to a user should update the cache', (done) => {
Parse.Cloud.define('testUpdatedUser', (req, res) => {
expect(req.user.get('han')).toEqual('solo');
res.success({});
});
let user = new Parse.User();
user.setUsername('harrison');
user.setPassword('ford');
user.signUp().then(() => {
user.set('han', 'solo');
return user.save();
}).then(() => {
return Parse.Cloud.run('testUpdatedUser');
}).then(() => {
done();
}, (e) => {
fail('Should not have failed.');
done();
});
});
it('should fail to become user with expired token', (done) => {
Parse.User.signUp("auser", "somepass", null, {
success: function(user) {
request.get({
url: 'http://localhost:8378/1/classes/_Session',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
}, (error, response, body) => {
var id = body.results[0].objectId;
var expiresAt = new Date((new Date()).setYear(2015));
var token = body.results[0].sessionToken;
request.put({
url: "http://localhost:8378/1/classes/_Session/" + id,
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
body: {
expiresAt: { __type: "Date", iso: expiresAt.toISOString() },
},
}, (error, response, body) => {
Parse.User.become(token)
.then(() => { fail("Should not have succeded"); })
.fail((err) => {
expect(err.code).toEqual(209);
expect(err.message).toEqual("Session token is expired.");
Parse.User.logOut() // Logout to prevent polluting CLI with messages
.then(done());
});
});
});
}
});
});
});
+43
View File
@@ -0,0 +1,43 @@
var ParseWebSocket = require('../src/LiveQuery/ParseWebSocketServer').ParseWebSocket;
describe('ParseWebSocket', function() {
it('can be initialized', function() {
var ws = {};
var parseWebSocket = new ParseWebSocket(ws);
expect(parseWebSocket.ws).toBe(ws);
});
it('can handle events defined in typeMap', function() {
var ws = {
on: jasmine.createSpy('on')
};
var callback = {};
var parseWebSocket = new ParseWebSocket(ws);
parseWebSocket.on('disconnect', callback);
expect(parseWebSocket.ws.on).toHaveBeenCalledWith('close', callback);
});
it('can handle events which are not defined in typeMap', function() {
var ws = {
on: jasmine.createSpy('on')
};
var callback = {};
var parseWebSocket = new ParseWebSocket(ws);
parseWebSocket.on('open', callback);
expect(parseWebSocket.ws.on).toHaveBeenCalledWith('open', callback);
});
it('can send a message', function() {
var ws = {
send: jasmine.createSpy('send')
};
var parseWebSocket = new ParseWebSocket(ws);
parseWebSocket.send('message')
expect(parseWebSocket.ws.send).toHaveBeenCalledWith('message');
});
});
+37
View File
@@ -0,0 +1,37 @@
var ParseWebSocketServer = require('../src/LiveQuery/ParseWebSocketServer').ParseWebSocketServer;
describe('ParseWebSocketServer', function() {
beforeEach(function(done) {
// Mock ws server
var EventEmitter = require('events');
var mockServer = function() {
return new EventEmitter();
};
jasmine.mockLibrary('ws', 'Server', mockServer);
done();
});
it('can handle connect event when ws is open', function(done) {
var onConnectCallback = jasmine.createSpy('onConnectCallback');
var parseWebSocketServer = new ParseWebSocketServer({}, onConnectCallback, 5).server;
var ws = {
readyState: 0,
OPEN: 0,
ping: jasmine.createSpy('ping')
};
parseWebSocketServer.emit('connection', ws);
// Make sure callback is called
expect(onConnectCallback).toHaveBeenCalled();
// Make sure we ping to the client
setTimeout(function() {
expect(ws.ping).toHaveBeenCalled();
done();
}, 10)
});
afterEach(function(){
jasmine.restoreLibrary('ws', 'Server');
});
});
+26
View File
@@ -0,0 +1,26 @@
var PromiseRouter = require("../src/PromiseRouter").default;
describe("PromiseRouter", () => {
it("should properly handle rejects", (done) => {
var router = new PromiseRouter();
router.route("GET", "/dummy", (req)=> {
return Promise.reject({
error: "an error",
code: -1
})
}, (req) => {
fail("this should not be called");
});
router.routes[0].handler({}).then((result) => {
console.error(result);
fail("this should not be called");
done();
}, (error)=> {
expect(error.error).toEqual("an error");
expect(error.code).toEqual(-1);
done();
});
});
})
+86
View File
@@ -0,0 +1,86 @@
var request = require('request');
describe("public API", () => {
beforeEach(done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
publicServerURL: 'http://localhost:8378/1'
});
done();
})
it("should get invalid_link.html", (done) => {
request('http://localhost:8378/1/apps/invalid_link.html', (err, httpResponse, body) => {
expect(httpResponse.statusCode).toBe(200);
done();
});
});
it("should get choose_password", (done) => {
request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse, body) => {
expect(httpResponse.statusCode).toBe(200);
done();
});
});
it("should get verify_email_success.html", (done) => {
request('http://localhost:8378/1/apps/verify_email_success.html', (err, httpResponse, body) => {
expect(httpResponse.statusCode).toBe(200);
done();
});
});
it("should get password_reset_success.html", (done) => {
request('http://localhost:8378/1/apps/password_reset_success.html', (err, httpResponse, body) => {
expect(httpResponse.statusCode).toBe(200);
done();
});
});
});
describe("public API without publicServerURL", () => {
beforeEach(done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
});
done();
})
it("should get 404 on verify_email", (done) => {
request('http://localhost:8378/1/apps/test/verify_email', (err, httpResponse, body) => {
expect(httpResponse.statusCode).toBe(404);
done();
});
});
it("should get 404 choose_password", (done) => {
request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse, body) => {
expect(httpResponse.statusCode).toBe(404);
done();
});
});
it("should get 404 on request_password_reset", (done) => {
request('http://localhost:8378/1/apps/test/request_password_reset', (err, httpResponse, body) => {
expect(httpResponse.statusCode).toBe(404);
done();
});
});
});
+209
View File
@@ -0,0 +1,209 @@
var request = require("request");
function createProduct() {
const file = new Parse.File("name", {
base64: new Buffer("download_file", "utf-8").toString("base64")
}, "text");
return file.save().then(function(){
var product = new Parse.Object("_Product");
product.set({
download: file,
icon: file,
title: "a product",
subtitle: "a product",
order: 1,
productIdentifier: "a-product"
})
return product.save();
})
}
describe("test validate_receipt endpoint", () => {
beforeEach( done => {
createProduct().then(done).fail(function(err){
console.error(err);
done();
})
})
it("should bypass appstore validation", (done) => {
request.post({
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'},
url: 'http://localhost:8378/1/validate_purchase',
json: true,
body: {
productIdentifier: "a-product",
receipt: {
__type: "Bytes",
base64: new Buffer("receipt", "utf-8").toString("base64")
},
bypassAppStoreValidation: true
}
}, function(err, res, body){
if (typeof body != "object") {
fail("Body is not an object");
done();
} else {
expect(body.__type).toEqual("File");
const url = body.url;
request.get({
url: url
}, function(err, res, body) {
expect(body).toEqual("download_file");
done();
});
}
});
});
it("should fail for missing receipt", (done) => {
request.post({
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'},
url: 'http://localhost:8378/1/validate_purchase',
json: true,
body: {
productIdentifier: "a-product",
bypassAppStoreValidation: true
}
}, function(err, res, body){
if (typeof body != "object") {
fail("Body is not an object");
done();
} else {
expect(body.code).toEqual(Parse.Error.INVALID_JSON);
done();
}
});
});
it("should fail for missing product identifier", (done) => {
request.post({
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'},
url: 'http://localhost:8378/1/validate_purchase',
json: true,
body: {
receipt: {
__type: "Bytes",
base64: new Buffer("receipt", "utf-8").toString("base64")
},
bypassAppStoreValidation: true
}
}, function(err, res, body){
if (typeof body != "object") {
fail("Body is not an object");
done();
} else {
expect(body.code).toEqual(Parse.Error.INVALID_JSON);
done();
}
});
});
it("should bypass appstore validation and not find product", (done) => {
request.post({
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'},
url: 'http://localhost:8378/1/validate_purchase',
json: true,
body: {
productIdentifier: "another-product",
receipt: {
__type: "Bytes",
base64: new Buffer("receipt", "utf-8").toString("base64")
},
bypassAppStoreValidation: true
}
}, function(err, res, body){
if (typeof body != "object") {
fail("Body is not an object");
done();
} else {
expect(body.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
expect(body.error).toEqual('Object not found.');
done();
}
});
});
it("should fail at appstore validation", (done) => {
request.post({
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'},
url: 'http://localhost:8378/1/validate_purchase',
json: true,
body: {
productIdentifier: "a-product",
receipt: {
__type: "Bytes",
base64: new Buffer("receipt", "utf-8").toString("base64")
},
}
}, function(err, res, body){
if (typeof body != "object") {
fail("Body is not an object");
} else {
expect(body.status).toBe(21002);
expect(body.error).toBe('The data in the receipt-data property was malformed or missing.');
}
done();
});
});
it("should not create a _Product", (done) => {
var product = new Parse.Object("_Product");
product.save().then(function(){
fail("Should not be able to save");
done();
}, function(err){
expect(err.code).toEqual(Parse.Error.INCORRECT_TYPE);
done();
})
});
it("should be able to update a _Product", (done) => {
var query = new Parse.Query("_Product");
query.first().then(function(product){
product.set("title", "a new title");
return product.save();
}).then(function(productAgain){
expect(productAgain.get('downloadName')).toEqual(productAgain.get('download').name());
expect(productAgain.get("title")).toEqual("a new title");
done();
}).fail(function(err){
fail(JSON.stringify(err));
done();
});
});
it("should not be able to remove a require key in a _Product", (done) => {
var query = new Parse.Query("_Product");
query.first().then(function(product){
product.unset("title");
return product.save();
}).then(function(productAgain){
fail("Should not succeed");
done();
}).fail(function(err){
expect(err.code).toEqual(Parse.Error.INCORRECT_TYPE);
expect(err.message).toEqual("title is required.");
done();
});
});
});
+307 -115
View File
@@ -1,106 +1,33 @@
"use strict";
var PushController = require('../src/Controllers/PushController').PushController;
var pushStatusHandler = require('../src/pushStatusHandler');
var Config = require('../src/Config');
const successfulTransmissions = function(body, installations) {
let promises = installations.map((device) => {
return Promise.resolve({
transmitted: true,
device: device,
})
});
return Promise.all(promises);
}
const successfulIOS = function(body, installations) {
let promises = installations.map((device) => {
return Promise.resolve({
transmitted: device.deviceType == "ios",
device: device,
})
});
return Promise.all(promises);
}
describe('PushController', () => {
it('can check valid master key of request', (done) => {
// Make mock request
var request = {
info: {
masterKey: 'masterKey'
},
config: {
masterKey: 'masterKey'
}
}
expect(() => {
PushController.validateMasterKey(request);
}).not.toThrow();
done();
});
it('can check invalid master key of request', (done) => {
// Make mock request
var request = {
info: {
masterKey: 'masterKey'
},
config: {
masterKey: 'masterKeyAgain'
}
}
expect(() => {
PushController.validateMasterKey(request);
}).toThrow();
done();
});
it('can get query condition when channels is set', (done) => {
// Make mock request
var request = {
body: {
channels: ['Giants', 'Mets']
}
}
var where = PushController.getQueryCondition(request);
expect(where).toEqual({
'channels': {
'$in': ['Giants', 'Mets']
}
});
done();
});
it('can get query condition when where is set', (done) => {
// Make mock request
var request = {
body: {
'where': {
'injuryReports': true
}
}
}
var where = PushController.getQueryCondition(request);
expect(where).toEqual({
'injuryReports': true
});
done();
});
it('can get query condition when nothing is set', (done) => {
// Make mock request
var request = {
body: {
}
}
expect(function() {
PushController.getQueryCondition(request);
}).toThrow();
done();
});
it('can throw on getQueryCondition when channels and where are set', (done) => {
// Make mock request
var request = {
body: {
'channels': {
'$in': ['Giants', 'Mets']
},
'where': {
'injuryReports': true
}
}
}
expect(function() {
PushController.getQueryCondition(request);
}).toThrow();
done();
});
it('can validate device type when no device type is set', (done) => {
// Make query condition
var where = {
@@ -170,13 +97,11 @@ describe('PushController', () => {
it('can get expiration time in string format', (done) => {
// Make mock request
var timeStr = '2015-03-19T22:05:08Z';
var request = {
body: {
var body = {
'expiration_time': timeStr
}
}
}
var time = PushController.getExpirationTime(request);
var time = PushController.getExpirationTime(body);
expect(time).toEqual(new Date(timeStr).valueOf());
done();
});
@@ -184,28 +109,295 @@ describe('PushController', () => {
it('can get expiration time in number format', (done) => {
// Make mock request
var timeNumber = 1426802708;
var request = {
body: {
'expiration_time': timeNumber
}
var body = {
'expiration_time': timeNumber
}
var time = PushController.getExpirationTime(request);
var time = PushController.getExpirationTime(body);
expect(time).toEqual(timeNumber * 1000);
done();
});
it('can throw on getExpirationTime in invalid format', (done) => {
// Make mock request
var request = {
body: {
'expiration_time': 'abcd'
}
var body = {
'expiration_time': 'abcd'
}
expect(function(){
PushController.getExpirationTime(request);
PushController.getExpirationTime(body);
}).toThrow();
done();
});
it('properly increment badges', (done) => {
var payload = {data:{
alert: "Hello World!",
badge: "Increment",
}}
var installations = [];
while(installations.length != 10) {
var installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_"+installations.length);
installation.set("deviceToken","device_token_"+installations.length)
installation.set("badge", installations.length);
installation.set("originalBadge", installations.length);
installation.set("deviceType", "ios");
installations.push(installation);
}
while(installations.length != 15) {
var installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_"+installations.length);
installation.set("deviceToken","device_token_"+installations.length)
installation.set("deviceType", "android");
installations.push(installation);
}
var pushAdapter = {
send: function(body, installations) {
var badge = body.data.badge;
installations.forEach((installation) => {
if (installation.deviceType == "ios") {
expect(installation.badge).toEqual(badge);
expect(installation.originalBadge+1).toEqual(installation.badge);
} else {
expect(installation.badge).toBeUndefined();
}
})
return successfulTransmissions(body, installations);
},
getValidPushTypes: function() {
return ["ios", "android"];
}
}
var config = new Config(Parse.applicationId);
var auth = {
isMaster: true
}
var pushController = new PushController(pushAdapter, Parse.applicationId);
Parse.Object.saveAll(installations).then((installations) => {
return pushController.sendPush(payload, {}, config, auth);
}).then((result) => {
done();
}, (err) => {
console.error(err);
fail("should not fail");
done();
});
});
it('properly set badges to 1', (done) => {
var payload = {data: {
alert: "Hello World!",
badge: 1,
}}
var installations = [];
while(installations.length != 10) {
var installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_"+installations.length);
installation.set("deviceToken","device_token_"+installations.length)
installation.set("badge", installations.length);
installation.set("originalBadge", installations.length);
installation.set("deviceType", "ios");
installations.push(installation);
}
var pushAdapter = {
send: function(body, installations) {
var badge = body.data.badge;
installations.forEach((installation) => {
expect(installation.badge).toEqual(badge);
expect(1).toEqual(installation.badge);
})
return successfulTransmissions(body, installations);
},
getValidPushTypes: function() {
return ["ios"];
}
}
var config = new Config(Parse.applicationId);
var auth = {
isMaster: true
}
var pushController = new PushController(pushAdapter, Parse.applicationId);
Parse.Object.saveAll(installations).then((installations) => {
return pushController.sendPush(payload, {}, config, auth);
}).then((result) => {
done();
}, (err) => {
console.error(err);
fail("should not fail");
done();
});
});
it('properly creates _PushStatus', (done) => {
var installations = [];
while(installations.length != 10) {
var installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_"+installations.length);
installation.set("deviceToken","device_token_"+installations.length)
installation.set("badge", installations.length);
installation.set("originalBadge", installations.length);
installation.set("deviceType", "ios");
installations.push(installation);
}
while(installations.length != 15) {
var installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_"+installations.length);
installation.set("deviceToken","device_token_"+installations.length)
installation.set("deviceType", "android");
installations.push(installation);
}
var payload = {data: {
alert: "Hello World!",
badge: 1,
}}
var pushAdapter = {
send: function(body, installations) {
return successfulIOS(body, installations);
},
getValidPushTypes: function() {
return ["ios"];
}
}
var config = new Config(Parse.applicationId);
var auth = {
isMaster: true
}
var pushController = new PushController(pushAdapter, Parse.applicationId);
Parse.Object.saveAll(installations).then(() => {
return pushController.sendPush(payload, {}, config, auth);
}).then((result) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 1000);
});
}).then(() => {
let query = new Parse.Query('_PushStatus');
return query.find({useMasterKey: true});
}).then((results) => {
expect(results.length).toBe(1);
let result = results[0];
expect(result.createdAt instanceof Date).toBe(true);
expect(result.get('source')).toEqual('rest');
expect(result.get('query')).toEqual(JSON.stringify({}));
expect(result.get('payload')).toEqual(payload.data);
expect(result.get('status')).toEqual('succeeded');
expect(result.get('numSent')).toEqual(10);
expect(result.get('sentPerType')).toEqual({
'ios': 10 // 10 ios
});
expect(result.get('numFailed')).toEqual(5);
expect(result.get('failedPerType')).toEqual({
'android': 5 // android
});
// Try to get it without masterKey
let query = new Parse.Query('_PushStatus');
return query.find();
}).then((results) => {
expect(results.length).toBe(0);
done();
});
});
it('should properly report failures in _PushStatus', (done) => {
var pushAdapter = {
send: function(body, installations) {
return installations.map((installation) => {
return Promise.resolve({
deviceType: installation.deviceType
})
})
},
getValidPushTypes: function() {
return ["ios"];
}
}
let where = { 'channels': {
'$ins': ['Giants', 'Mets']
}};
var payload = {data: {
alert: "Hello World!",
badge: 1,
}}
var config = new Config(Parse.applicationId);
var auth = {
isMaster: true
}
var pushController = new PushController(pushAdapter, Parse.applicationId);
pushController.sendPush(payload, where, config, auth).then(() => {
fail('should not succeed');
done();
}).catch(() => {
let query = new Parse.Query('_PushStatus');
query.find({useMasterKey: true}).then((results) => {
expect(results.length).toBe(1);
let pushStatus = results[0];
expect(pushStatus.get('status')).toBe('failed');
done();
});
})
});
it('should support full RESTQuery for increment', (done) => {
var payload = {data: {
alert: "Hello World!",
badge: 'Increment',
}}
var pushAdapter = {
send: function(body, installations) {
return successfulTransmissions(body, installations);
},
getValidPushTypes: function() {
return ["ios"];
}
}
var config = new Config(Parse.applicationId);
var auth = {
isMaster: true
}
let where = {
'deviceToken': {
'$inQuery': {
'where': {
'deviceType': 'ios'
},
className: '_Installation'
}
}
}
var pushController = new PushController(pushAdapter, Parse.applicationId);
pushController.sendPush(payload, where, config, auth).then((result) => {
done();
}).catch((err) => {
fail('should not fail');
done();
});
});
it('should flatten', () => {
var res = pushStatusHandler.flatten([1, [2], [[3, 4], 5], [[[6]]]])
expect(res).toEqual([1,2,3,4,5,6]);
})
});
+89
View File
@@ -0,0 +1,89 @@
var PushRouter = require('../src/Routers/PushRouter').PushRouter;
var request = require('request');
describe('PushRouter', () => {
it('can get query condition when channels is set', (done) => {
// Make mock request
var request = {
body: {
channels: ['Giants', 'Mets']
}
}
var where = PushRouter.getQueryCondition(request);
expect(where).toEqual({
'channels': {
'$in': ['Giants', 'Mets']
}
});
done();
});
it('can get query condition when where is set', (done) => {
// Make mock request
var request = {
body: {
'where': {
'injuryReports': true
}
}
}
var where = PushRouter.getQueryCondition(request);
expect(where).toEqual({
'injuryReports': true
});
done();
});
it('can get query condition when nothing is set', (done) => {
// Make mock request
var request = {
body: {
}
}
expect(function() {
PushRouter.getQueryCondition(request);
}).toThrow();
done();
});
it('can throw on getQueryCondition when channels and where are set', (done) => {
// Make mock request
var request = {
body: {
'channels': {
'$in': ['Giants', 'Mets']
},
'where': {
'injuryReports': true
}
}
}
expect(function() {
PushRouter.getQueryCondition(request);
}).toThrow();
done();
});
it('sends a push through REST', (done) => {
request.post({
url: Parse.serverURL+"/push",
json: true,
body: {
'channels': {
'$in': ['Giants', 'Mets']
}
},
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey
}
}, function(err, res, body){
expect(body.result).toBe(true);
done();
});
});
});
+398
View File
@@ -0,0 +1,398 @@
var Parse = require('parse/node');
var Id = require('../src/LiveQuery/Id');
var QueryTools = require('../src/LiveQuery/QueryTools');
var queryHash = QueryTools.queryHash;
var matchesQuery = QueryTools.matchesQuery;
var Item = Parse.Object.extend('Item');
describe('queryHash', function() {
it('should always hash a query to the same string', function() {
var q = new Parse.Query(Item);
q.equalTo('field', 'value');
q.exists('name');
q.ascending('createdAt');
q.limit(10);
var firstHash = queryHash(q);
var secondHash = queryHash(q);
expect(firstHash).toBe(secondHash);
});
it('should return equivalent hashes for equivalent queries', function() {
var q1 = new Parse.Query(Item);
q1.equalTo('field', 'value');
q1.exists('name');
q1.lessThan('age', 30);
q1.greaterThan('age', 3);
q1.ascending('createdAt');
q1.include(['name', 'age']);
q1.limit(10);
var q2 = new Parse.Query(Item);
q2.limit(10);
q2.greaterThan('age', 3);
q2.lessThan('age', 30);
q2.include(['name', 'age']);
q2.ascending('createdAt');
q2.exists('name');
q2.equalTo('field', 'value');
var firstHash = queryHash(q1);
var secondHash = queryHash(q2);
expect(firstHash).toBe(secondHash);
q1.containedIn('fruit', ['apple', 'banana', 'cherry']);
firstHash = queryHash(q1);
expect(firstHash).not.toBe(secondHash);
q2.containedIn('fruit', ['banana', 'cherry', 'apple']);
secondHash = queryHash(q2);
expect(secondHash).toBe(firstHash);
q1.containedIn('fruit', ['coconut']);
firstHash = queryHash(q1);
expect(firstHash).not.toBe(secondHash);
q1 = new Parse.Query(Item);
q1.equalTo('field', 'value');
q1.lessThan('age', 30);
q1.exists('name');
q2 = new Parse.Query(Item);
q2.equalTo('name', 'person');
q2.equalTo('field', 'other');
firstHash = queryHash(Parse.Query.or(q1, q2));
secondHash = queryHash(Parse.Query.or(q2, q1));
expect(firstHash).toBe(secondHash);
});
it('should not let fields of different types appear similar', function() {
var q1 = new Parse.Query(Item);
q1.lessThan('age', 30);
var q2 = new Parse.Query(Item);
q2.equalTo('age', '{$lt:30}');
expect(queryHash(q1)).not.toBe(queryHash(q2));
q1 = new Parse.Query(Item);
q1.equalTo('age', 15);
q2.equalTo('age', '15');
expect(queryHash(q1)).not.toBe(queryHash(q2));
});
});
describe('matchesQuery', function() {
it('matches blanket queries', function() {
var obj = {
id: new Id('Klass', 'O1'),
value: 12
};
var q = new Parse.Query('Klass');
expect(matchesQuery(obj, q)).toBe(true);
obj.id = new Id('Other', 'O1');
expect(matchesQuery(obj, q)).toBe(false);
});
it('matches existence queries', function() {
var obj = {
id: new Id('Item', 'O1'),
count: 15
};
var q = new Parse.Query('Item');
q.exists('count');
expect(matchesQuery(obj, q)).toBe(true);
q.exists('name');
expect(matchesQuery(obj, q)).toBe(false);
});
it('matches queries with doesNotExist constraint', function() {
var obj = {
id: new Id('Item', 'O1'),
count: 15
};
var q = new Parse.Query('Item');
q.doesNotExist('name');
expect(matchesQuery(obj, q)).toBe(true);
q = new Parse.Query('Item');
q.doesNotExist('count');
expect(matchesQuery(obj, q)).toBe(false);
});
it('matches on equality queries', function() {
var day = new Date();
var location = new Parse.GeoPoint({
latitude: 37.484815,
longitude: -122.148377
});
var obj = {
id: new Id('Person', 'O1'),
score: 12,
name: 'Bill',
birthday: day,
lastLocation: location
};
var q = new Parse.Query('Person');
q.equalTo('score', 12);
expect(matchesQuery(obj, q)).toBe(true);
q = new Parse.Query('Person');
q.equalTo('name', 'Bill');
expect(matchesQuery(obj, q)).toBe(true);
q.equalTo('name', 'Jeff');
expect(matchesQuery(obj, q)).toBe(false);
q = new Parse.Query('Person');
q.containedIn('name', ['Adam', 'Ben', 'Charles']);
expect(matchesQuery(obj, q)).toBe(false);
q.containedIn('name', ['Adam', 'Bill', 'Charles']);
expect(matchesQuery(obj, q)).toBe(true);
q = new Parse.Query('Person');
q.notContainedIn('name', ['Adam', 'Bill', 'Charles']);
expect(matchesQuery(obj, q)).toBe(false);
q.notContainedIn('name', ['Adam', 'Ben', 'Charles']);
expect(matchesQuery(obj, q)).toBe(true);
q = new Parse.Query('Person');
q.equalTo('birthday', day);
expect(matchesQuery(obj, q)).toBe(true);
q.equalTo('birthday', new Date(1990, 1));
expect(matchesQuery(obj, q)).toBe(false);
q = new Parse.Query('Person');
q.equalTo('lastLocation', new Parse.GeoPoint({
latitude: 37.484815,
longitude: -122.148377
}));
expect(matchesQuery(obj, q)).toBe(true);
q.equalTo('lastLocation', new Parse.GeoPoint({
latitude: 37.4848,
longitude: -122.1483
}));
expect(matchesQuery(obj, q)).toBe(false);
q.equalTo('lastLocation', new Parse.GeoPoint({
latitude: 37.484815,
longitude: -122.148377
}));
q.equalTo('score', 12);
q.equalTo('name', 'Bill');
q.equalTo('birthday', day);
expect(matchesQuery(obj, q)).toBe(true);
q.equalTo('name', 'bill');
expect(matchesQuery(obj, q)).toBe(false);
var img = {
id: new Id('Image', 'I1'),
tags: ['nofilter', 'latergram', 'tbt']
};
q = new Parse.Query('Image');
q.equalTo('tags', 'selfie');
expect(matchesQuery(img, q)).toBe(false);
q.equalTo('tags', 'tbt');
expect(matchesQuery(img, q)).toBe(true);
var q2 = new Parse.Query('Image');
q2.containsAll('tags', ['latergram', 'nofilter']);
expect(matchesQuery(img, q2)).toBe(true);
q2.containsAll('tags', ['latergram', 'selfie']);
expect(matchesQuery(img, q2)).toBe(false);
var u = new Parse.User();
u.id = 'U2';
q = new Parse.Query('Image');
q.equalTo('owner', u);
img = {
className: 'Image',
objectId: 'I1',
owner: {
className: '_User',
objectId: 'U2'
}
};
expect(matchesQuery(img, q)).toBe(true);
img.owner.objectId = 'U3';
expect(matchesQuery(img, q)).toBe(false);
});
it('matches on inequalities', function() {
var player = {
id: new Id('Person', 'O1'),
score: 12,
name: 'Bill',
birthday: new Date(1980, 2, 4),
};
var q = new Parse.Query('Person');
q.lessThan('score', 15);
expect(matchesQuery(player, q)).toBe(true);
q.lessThan('score', 10);
expect(matchesQuery(player, q)).toBe(false);
q = new Parse.Query('Person');
q.lessThanOrEqualTo('score', 15);
expect(matchesQuery(player, q)).toBe(true);
q.lessThanOrEqualTo('score', 12);
expect(matchesQuery(player, q)).toBe(true);
q.lessThanOrEqualTo('score', 10);
expect(matchesQuery(player, q)).toBe(false);
q = new Parse.Query('Person');
q.greaterThan('score', 15);
expect(matchesQuery(player, q)).toBe(false);
q.greaterThan('score', 10);
expect(matchesQuery(player, q)).toBe(true);
q = new Parse.Query('Person');
q.greaterThanOrEqualTo('score', 15);
expect(matchesQuery(player, q)).toBe(false);
q.greaterThanOrEqualTo('score', 12);
expect(matchesQuery(player, q)).toBe(true);
q.greaterThanOrEqualTo('score', 10);
expect(matchesQuery(player, q)).toBe(true);
q = new Parse.Query('Person');
q.notEqualTo('score', 12);
expect(matchesQuery(player, q)).toBe(false);
q.notEqualTo('score', 40);
expect(matchesQuery(player, q)).toBe(true);
});
it('matches an $or query', function() {
var player = {
id: new Id('Player', 'P1'),
name: 'Player 1',
score: 12
};
var q = new Parse.Query('Player');
q.equalTo('name', 'Player 1');
var q2 = new Parse.Query('Player');
q2.equalTo('name', 'Player 2');
var orQuery = Parse.Query.or(q, q2);
expect(matchesQuery(player, q)).toBe(true);
expect(matchesQuery(player, q2)).toBe(false);
expect(matchesQuery(player, orQuery)).toBe(true);
});
it('matches $regex queries', function() {
var player = {
id: new Id('Player', 'P1'),
name: 'Player 1',
score: 12
};
var q = new Parse.Query('Player');
q.startsWith('name', 'Play');
expect(matchesQuery(player, q)).toBe(true);
q.startsWith('name', 'Ploy');
expect(matchesQuery(player, q)).toBe(false);
q = new Parse.Query('Player');
q.endsWith('name', ' 1');
expect(matchesQuery(player, q)).toBe(true);
q.endsWith('name', ' 2');
expect(matchesQuery(player, q)).toBe(false);
// Check that special characters are escaped
player.name = 'Android-7';
q = new Parse.Query('Player');
q.contains('name', 'd-7');
expect(matchesQuery(player, q)).toBe(true);
q = new Parse.Query('Player');
q.matches('name', /A.d/);
expect(matchesQuery(player, q)).toBe(true);
q.matches('name', /A[^n]d/);
expect(matchesQuery(player, q)).toBe(false);
// Check that the string \\E is returned to normal
player.name = 'Slash \\E';
q = new Parse.Query('Player');
q.endsWith('name', 'h \\E');
expect(matchesQuery(player, q)).toBe(true);
q.endsWith('name', 'h \\Ee');
expect(matchesQuery(player, q)).toBe(false);
player.name = 'Slash \\Q and more';
q = new Parse.Query('Player');
q.contains('name', 'h \\Q and');
expect(matchesQuery(player, q)).toBe(true);
q.contains('name', 'h \\Q or');
expect(matchesQuery(player, q)).toBe(false);
});
it('matches $nearSphere queries', function() {
var q = new Parse.Query('Checkin');
q.near('location', new Parse.GeoPoint(20, 20));
// With no max distance, any GeoPoint is 'near'
var pt = {
id: new Id('Checkin', 'C1'),
location: new Parse.GeoPoint(40, 40)
};
expect(matchesQuery(pt, q)).toBe(true);
q = new Parse.Query('Checkin');
pt.location = new Parse.GeoPoint(40, 40);
q.withinRadians('location', new Parse.GeoPoint(30, 30), 0.3);
expect(matchesQuery(pt, q)).toBe(true);
q.withinRadians('location', new Parse.GeoPoint(30, 30), 0.2);
expect(matchesQuery(pt, q)).toBe(false);
});
it('matches $within queries', function() {
var caltrainStation = {
id: new Id('Checkin', 'C1'),
location: new Parse.GeoPoint(37.776346, -122.394218),
name: 'Caltrain'
};
var santaClara = {
id: new Id('Checkin', 'C2'),
location: new Parse.GeoPoint(37.325635, -121.945753),
name: 'Santa Clara'
};
var q = new Parse.Query('Checkin').withinGeoBox(
'location',
new Parse.GeoPoint(37.708813, -122.526398),
new Parse.GeoPoint(37.822802, -122.373962)
);
expect(matchesQuery(caltrainStation, q)).toBe(true);
expect(matchesQuery(santaClara, q)).toBe(false);
// Invalid rectangles
q = new Parse.Query('Checkin').withinGeoBox(
'location',
new Parse.GeoPoint(37.822802, -122.373962),
new Parse.GeoPoint(37.708813, -122.526398)
);
expect(matchesQuery(caltrainStation, q)).toBe(false);
expect(matchesQuery(santaClara, q)).toBe(false);
q = new Parse.Query('Checkin').withinGeoBox(
'location',
new Parse.GeoPoint(37.708813, -122.373962),
new Parse.GeoPoint(37.822802, -122.526398)
);
expect(matchesQuery(caltrainStation, q)).toBe(false);
expect(matchesQuery(santaClara, q)).toBe(false);
});
});
+29
View File
@@ -0,0 +1,29 @@
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
describe('RedisPubSub', function() {
beforeEach(function(done) {
// Mock redis
var createClient = jasmine.createSpy('createClient');
jasmine.mockLibrary('redis', 'createClient', createClient);
done();
});
it('can create publisher', function() {
var publisher = RedisPubSub.createPublisher('redisAddress');
var redis = require('redis');
expect(redis.createClient).toHaveBeenCalledWith('redisAddress', { no_ready_check: true });
});
it('can create subscriber', function() {
var subscriber = RedisPubSub.createSubscriber('redisAddress');
var redis = require('redis');
expect(redis.createClient).toHaveBeenCalledWith('redisAddress', { no_ready_check: true });
});
afterEach(function() {
jasmine.restoreLibrary('redis', 'createClient');
});
});
+168 -4
View File
@@ -1,4 +1,4 @@
// These tests check the "create" functionality of the REST API.
// These tests check the "create" / "update" functionality of the REST API.
var auth = require('../src/Auth');
var cache = require('../src/cache');
var Config = require('../src/Config');
@@ -8,7 +8,7 @@ var rest = require('../src/rest');
var request = require('request');
var config = new Config('test');
var database = DatabaseAdapter.getDatabaseConnection('test');
var database = DatabaseAdapter.getDatabaseConnection('test', 'test_');
describe('rest create', () => {
it('handles _id', (done) => {
@@ -41,6 +41,52 @@ describe('rest create', () => {
});
});
it('handles object and subdocument', (done) => {
var obj = {
subdoc: {foo: 'bar', wu: 'tan'},
};
rest.create(config, auth.nobody(config), 'MyClass', obj).then(() => {
return database.mongoFind('MyClass', {}, {});
}).then((results) => {
expect(results.length).toEqual(1);
var mob = results[0];
expect(typeof mob.subdoc).toBe('object');
expect(mob.subdoc.foo).toBe('bar');
expect(mob.subdoc.wu).toBe('tan');
expect(typeof mob._id).toEqual('string');
var obj = {
'subdoc.wu': 'clan',
};
rest.update(config, auth.nobody(config), 'MyClass', mob._id, obj).then(() => {
return database.mongoFind('MyClass', {}, {});
}).then((results) => {
expect(results.length).toEqual(1);
var mob = results[0];
expect(typeof mob.subdoc).toBe('object');
expect(mob.subdoc.foo).toBe('bar');
expect(mob.subdoc.wu).toBe('clan');
done();
});
});
});
it('handles create on non-existent class when disabled client class creation', (done) => {
var customConfig = Object.assign({}, config, {allowClientClassCreation: false});
rest.create(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {})
.then(() => {
fail('Should throw an error');
done();
}, (err) => {
expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
expect(err.message).toEqual('This user is not allowed to access ' +
'non-existent class: ClientClassCreation');
done();
});
});
it('handles user signup', (done) => {
var user = {
username: 'asdf',
@@ -100,9 +146,49 @@ describe('rest create', () => {
done();
});
});
it('handles anonymous user signup and upgrade to new user', (done) => {
var data1 = {
authData: {
anonymous: {
id: '00000000-0000-0000-0000-000000000001'
}
}
};
var updatedData = {
authData: { anonymous: null },
username: 'hello',
password: 'world'
}
var username1;
var objectId;
rest.create(config, auth.nobody(config), '_User', data1)
.then((r) => {
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.sessionToken).toEqual('string');
objectId = r.response.objectId;
return auth.getAuthForSessionToken({config, sessionToken: r.response.sessionToken })
}).then((sessionAuth) => {
return rest.update(config, sessionAuth, '_User', objectId, updatedData);
}).then((r) => {
return Parse.User.logOut().then(() => {
return Parse.User.logIn('hello', 'world');
})
}).then((r) => {
expect(r.id).toEqual(objectId);
expect(r.get('username')).toEqual('hello');
done();
}).catch((err) => {
fail('should not fail')
done();
})
});
it('handles no anonymous users config', (done) => {
var NoAnnonConfig = Object.assign({}, config, {enableAnonymousUsers: false});
var NoAnnonConfig = Object.assign({}, config);
NoAnnonConfig.authDataManager.setEnableAnonymousUsers(false);
var data1 = {
authData: {
anonymous: {
@@ -116,6 +202,7 @@ describe('rest create', () => {
}, (err) => {
expect(err.code).toEqual(Parse.Error.UNSUPPORTED_SERVICE);
expect(err.message).toEqual('This authentication method is unsupported.');
NoAnnonConfig.authDataManager.setEnableAnonymousUsers(true);
done();
})
});
@@ -129,17 +216,26 @@ describe('rest create', () => {
}
}
};
var newUserSignedUpByFacebookObjectId;
rest.create(config, auth.nobody(config), '_User', data)
.then((r) => {
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.sessionToken).toEqual('string');
newUserSignedUpByFacebookObjectId = r.response.objectId;
return rest.create(config, auth.nobody(config), '_User', data);
}).then((r) => {
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.username).toEqual('string');
expect(typeof r.response.updatedAt).toEqual('string');
expect(r.response.objectId).toEqual(newUserSignedUpByFacebookObjectId);
return rest.find(config, auth.master(config),
'_Session', {sessionToken: r.response.sessionToken});
}).then((response) => {
expect(response.results.length).toEqual(1);
var output = response.results[0];
expect(output.user.objectId).toEqual(newUserSignedUpByFacebookObjectId);
done();
});
});
@@ -188,4 +284,72 @@ describe('rest create', () => {
});
});
it("test default session length", (done) => {
var user = {
username: 'asdf',
password: 'zxcv',
foo: 'bar',
};
var now = new Date();
rest.create(config, auth.nobody(config), '_User', user)
.then((r) => {
expect(Object.keys(r.response).length).toEqual(3);
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.sessionToken).toEqual('string');
return rest.find(config, auth.master(config),
'_Session', {sessionToken: r.response.sessionToken});
})
.then((r) => {
expect(r.results.length).toEqual(1);
var session = r.results[0];
var actual = new Date(session.expiresAt.iso);
var expected = new Date(now.getTime() + (1000 * 3600 * 24 * 365));
expect(actual.getFullYear()).toEqual(expected.getFullYear());
expect(actual.getMonth()).toEqual(expected.getMonth());
expect(actual.getDate()).toEqual(expected.getDate());
expect(actual.getMinutes()).toEqual(expected.getMinutes());
done();
});
});
it("test specified session length", (done) => {
var user = {
username: 'asdf',
password: 'zxcv',
foo: 'bar',
};
var sessionLength = 3600, // 1 Hour ahead
now = new Date(); // For reference later
config.sessionLength = sessionLength;
rest.create(config, auth.nobody(config), '_User', user)
.then((r) => {
expect(Object.keys(r.response).length).toEqual(3);
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.sessionToken).toEqual('string');
return rest.find(config, auth.master(config),
'_Session', {sessionToken: r.response.sessionToken});
})
.then((r) => {
expect(r.results.length).toEqual(1);
var session = r.results[0];
var actual = new Date(session.expiresAt.iso);
var expected = new Date(now.getTime() + (sessionLength*1000));
expect(actual.getFullYear()).toEqual(expected.getFullYear());
expect(actual.getMonth()).toEqual(expected.getMonth());
expect(actual.getDate()).toEqual(expected.getDate());
expect(actual.getHours()).toEqual(expected.getHours());
expect(actual.getMinutes()).toEqual(expected.getMinutes());
done();
});
});
});
+62
View File
@@ -4,6 +4,9 @@ var cache = require('../src/cache');
var Config = require('../src/Config');
var rest = require('../src/rest');
var querystring = require('querystring');
var request = require('request');
var config = new Config('test');
var nobody = auth.nobody(config);
@@ -92,4 +95,63 @@ describe('rest query', () => {
}).catch((error) => { console.log(error); });
});
it('query non-existent class when disabled client class creation', (done) => {
var customConfig = Object.assign({}, config, {allowClientClassCreation: false});
rest.find(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {})
.then(() => {
fail('Should throw an error');
done();
}, (err) => {
expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
expect(err.message).toEqual('This user is not allowed to access ' +
'non-existent class: ClientClassCreation');
done();
});
});
it('query with wrongly encoded parameter', (done) => {
rest.create(config, nobody, 'TestParameterEncode', {foo: 'bar'}
).then(() => {
return rest.create(config, nobody,
'TestParameterEncode', {foo: 'baz'});
}).then(() => {
var headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'
};
request.get({
headers: headers,
url: 'http://localhost:8378/1/classes/TestParameterEncode?'
+ querystring.stringify({
where: '{"foo":{"$ne": "baz"}}',
limit: 1
}).replace('=', '%3D'),
}, (error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(b.code).toEqual(Parse.Error.INVALID_QUERY);
expect(b.error).toEqual('Improper encode of parameter');
done();
});
}).then(() => {
var headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'
};
request.get({
headers: headers,
url: 'http://localhost:8378/1/classes/TestParameterEncode?'
+ querystring.stringify({
limit: 1
}).replace('=', '%3D'),
}, (error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(b.code).toEqual(Parse.Error.INVALID_QUERY);
expect(b.error).toEqual('Improper encode of parameter');
done();
});
});
});
});
+171 -40
View File
@@ -1,3 +1,5 @@
'use strict';
var Config = require('../src/Config');
var Schema = require('../src/Schema');
var dd = require('deep-diff');
@@ -14,9 +16,6 @@ var hasAllPODobject = () => {
obj.set('aArray', ['contents', true, 5]);
obj.set('aGeoPoint', new Parse.GeoPoint({latitude: 0, longitude: 0}));
obj.set('aFile', new Parse.File('f.txt', { base64: 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=' }));
var objACL = new Parse.ACL();
objACL.setPublicWriteAccess(false);
obj.setACL(objACL);
return obj;
};
@@ -32,6 +31,17 @@ describe('Schema', () => {
});
});
it('can validate one object with dot notation', (done) => {
config.database.loadSchema().then((schema) => {
return schema.validateObject('TestObjectWithSubDoc', {x: false, y: 'YY', z: 1, 'aObject.k1': 'newValue'});
}).then((schema) => {
done();
}, (error) => {
fail(error);
done();
});
});
it('can validate two objects in a row', (done) => {
config.database.loadSchema().then((schema) => {
return schema.validateObject('Foo', {x: true, y: 'yyy', z: 0});
@@ -170,20 +180,18 @@ describe('Schema', () => {
it('will fail to create a class if that class was already created by an object', done => {
config.database.loadSchema()
.then(schema => {
schema.validateObject('NewClass', {foo: 7})
.then(() => {
schema.reload()
.then(schema => schema.addClassIfNotExists('NewClass', {
foo: {type: 'String'}
}))
.catch(error => {
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME)
expect(error.error).toEqual('class NewClass already exists');
done();
});
.then(schema => {
schema.validateObject('NewClass', { foo: 7 })
.then(() => schema.reloadData())
.then(() => schema.addClassIfNotExists('NewClass', {
foo: { type: 'String' }
}))
.catch(error => {
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
expect(error.message).toEqual('Class NewClass already exists.');
done();
});
});
})
});
it('will resolve class creation races appropriately', done => {
@@ -206,7 +214,7 @@ describe('Schema', () => {
Promise.all([p1,p2])
.catch(error => {
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
expect(error.error).toEqual('class NewClass already exists');
expect(error.message).toEqual('Class NewClass already exists.');
done();
});
});
@@ -412,6 +420,43 @@ describe('Schema', () => {
});
});
it('creates non-custom classes which include relation field', done => {
config.database.loadSchema()
.then(schema => schema.addClassIfNotExists('_Role', {}))
.then(mongoObj => {
expect(mongoObj).toEqual({
_id: '_Role',
createdAt: 'string',
updatedAt: 'string',
objectId: 'string',
name: 'string',
users: 'relation<_User>',
roles: 'relation<_Role>',
});
done();
});
});
it('creates non-custom classes which include pointer field', done => {
config.database.loadSchema()
.then(schema => schema.addClassIfNotExists('_Session', {}))
.then(mongoObj => {
expect(mongoObj).toEqual({
_id: '_Session',
createdAt: 'string',
updatedAt: 'string',
objectId: 'string',
restricted: 'boolean',
user: '*_User',
installationId: 'string',
sessionToken: 'string',
expiresAt: 'date',
createdWith: 'object'
});
done();
});
});
it('refuses to create two geopoints', done => {
config.database.loadSchema()
.then(schema => schema.addClassIfNotExists('NewClass', {
@@ -475,7 +520,7 @@ describe('Schema', () => {
.then(schema => schema.deleteField('installationId', '_Installation'))
.catch(error => {
expect(error.code).toEqual(136);
expect(error.error).toEqual('field installationId cannot be changed');
expect(error.message).toEqual('field installationId cannot be changed');
done();
});
});
@@ -485,7 +530,7 @@ describe('Schema', () => {
.then(schema => schema.deleteField('field', 'NoClass'))
.catch(error => {
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
expect(error.error).toEqual('class NoClass does not exist');
expect(error.message).toEqual('Class NoClass does not exist.');
done();
});
});
@@ -496,7 +541,7 @@ describe('Schema', () => {
.then(schema => schema.deleteField('missingField', 'HasAllPOD'))
.fail(error => {
expect(error.code).toEqual(255);
expect(error.error).toEqual('field missingField does not exist, cannot delete');
expect(error.message).toEqual('Field missingField does not exist, cannot delete.');
done();
});
});
@@ -504,24 +549,64 @@ describe('Schema', () => {
it('drops related collection when deleting relation field', done => {
var obj1 = hasAllPODobject();
obj1.save()
.then(savedObj1 => {
var obj2 = new Parse.Object('HasPointersAndRelations');
obj2.set('aPointer', savedObj1);
var relation = obj2.relation('aRelation');
relation.add(obj1);
return obj2.save();
})
.then(() => {
config.database.db.collection('test__Join:aRelation:HasPointersAndRelations', { strict: true }, (err, coll) => {
expect(err).toEqual(null);
config.database.loadSchema()
.then(schema => schema.deleteField('aRelation', 'HasPointersAndRelations', config.database.db, 'test_'))
.then(() => config.database.db.collection('test__Join:aRelation:HasPointersAndRelations', { strict: true }, (err, coll) => {
expect(err).not.toEqual(null);
done();
}))
.then(savedObj1 => {
var obj2 = new Parse.Object('HasPointersAndRelations');
obj2.set('aPointer', savedObj1);
var relation = obj2.relation('aRelation');
relation.add(obj1);
return obj2.save();
})
.then(() => config.database.collectionExists('_Join:aRelation:HasPointersAndRelations'))
.then(exists => {
if (!exists) {
fail('Relation collection ' +
'should exist after save.');
}
})
.then(() => config.database.loadSchema())
.then(schema => schema.deleteField('aRelation', 'HasPointersAndRelations', config.database))
.then(() => config.database.collectionExists('_Join:aRelation:HasPointersAndRelations'))
.then(exists => {
if (exists) {
fail('Relation collection should not exist after deleting relation field.');
}
done();
}, error => {
fail(error);
done();
});
})
});
it('can delete relation field when related _Join collection not exist', done => {
config.database.loadSchema()
.then(schema => {
schema.addClassIfNotExists('NewClass', {
relationField: {type: 'Relation', targetClass: '_User'}
})
.then(mongoObj => {
expect(mongoObj).toEqual({
_id: 'NewClass',
objectId: 'string',
updatedAt: 'string',
createdAt: 'string',
relationField: 'relation<_User>',
});
})
.then(() => config.database.collectionExists('_Join:relationField:NewClass'))
.then(exist => {
expect(exist).toEqual(false);
})
.then(() => schema.deleteField('relationField', 'NewClass', config.database))
.then(() => schema.reloadData())
.then(() => {
expect(schema['data']['NewClass']).toEqual({
objectId: 'string',
updatedAt: 'string',
createdAt: 'string'
});
done();
});
});
});
it('can delete string fields and resave as number field', done => {
@@ -530,7 +615,7 @@ describe('Schema', () => {
var obj2 = hasAllPODobject();
var p = Parse.Object.saveAll([obj1, obj2])
.then(() => config.database.loadSchema())
.then(schema => schema.deleteField('aString', 'HasAllPOD', config.database.db, 'test_'))
.then(schema => schema.deleteField('aString', 'HasAllPOD', config.database))
.then(() => new Parse.Query('HasAllPOD').get(obj1.id))
.then(obj1Reloaded => {
expect(obj1Reloaded.get('aString')).toEqual(undefined);
@@ -545,7 +630,7 @@ describe('Schema', () => {
done();
Parse.Object.enableSingleInstance();
});
})
});
});
it('can delete pointer fields and resave as string', done => {
@@ -560,7 +645,7 @@ describe('Schema', () => {
expect(obj1.get('aPointer').id).toEqual(obj1.id);
})
.then(() => config.database.loadSchema())
.then(schema => schema.deleteField('aPointer', 'NewClass', config.database.db, 'test_'))
.then(schema => schema.deleteField('aPointer', 'NewClass', config.database))
.then(() => new Parse.Query('NewClass').get(obj1.id))
.then(obj1 => {
expect(obj1.get('aPointer')).toEqual(undefined);
@@ -601,4 +686,50 @@ describe('Schema', () => {
});
done();
});
it('ignore default field when merge with system class', done => {
expect(Schema.buildMergedSchemaObject({
_id: '_User',
username: 'string',
password: 'string',
authData: 'object',
email: 'string',
emailVerified: 'boolean'
},{
authData: {type: 'string'},
customField: {type: 'string'},
})).toEqual({
customField: {type: 'string'}
});
done();
});
it('handles legacy _client_permissions keys without crashing', done => {
Schema.mongoSchemaToSchemaAPIResponse({
"_id":"_Installation",
"_client_permissions":{
"get":true,
"find":true,
"update":true,
"create":true,
"delete":true,
},
"_metadata":{
"class_permissions":{
"get":{"*":true},
"find":{"*":true},
"update":{"*":true},
"create":{"*":true},
"delete":{"*":true},
"addField":{"*":true},
}
},
"installationId":"string",
"deviceToken":"string",
"deviceType":"string",
"channels":"array",
"user":"*_User",
});
done();
});
});
+52
View File
@@ -0,0 +1,52 @@
var SessionTokenCache = require('../src/LiveQuery/SessionTokenCache').SessionTokenCache;
describe('SessionTokenCache', function() {
beforeEach(function(done) {
var Parse = require('parse/node');
// Mock parse
var mockUser = {
become: jasmine.createSpy('become').and.returnValue(Parse.Promise.as({
id: 'userId'
}))
}
jasmine.mockLibrary('parse/node', 'User', mockUser);
done();
});
it('can get undefined userId', function(done) {
var sessionTokenCache = new SessionTokenCache();
sessionTokenCache.getUserId(undefined).then((userIdFromCache) => {
}, (error) => {
expect(error).not.toBeNull();
done();
});
});
it('can get existing userId', function(done) {
var sessionTokenCache = new SessionTokenCache();
var sessionToken = 'sessionToken';
var userId = 'userId'
sessionTokenCache.cache.set(sessionToken, userId);
sessionTokenCache.getUserId(sessionToken).then((userIdFromCache) => {
expect(userIdFromCache).toBe(userId);
done();
});
});
it('can get new userId', function(done) {
var sessionTokenCache = new SessionTokenCache();
sessionTokenCache.getUserId('sessionToken').then((userIdFromCache) => {
expect(userIdFromCache).toBe('userId');
expect(sessionTokenCache.cache.length).toBe(1);
done();
});
});
afterEach(function() {
jasmine.restoreLibrary('parse/node', 'User');
});
});
+123
View File
@@ -0,0 +1,123 @@
var Subscription = require('../src/LiveQuery/Subscription').Subscription;
describe('Subscription', function() {
beforeEach(function() {
var mockError = jasmine.createSpy('error');
jasmine.mockLibrary('../src/LiveQuery/PLog', 'error', mockError);
});
it('can be initialized', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
expect(subscription.className).toBe('className');
expect(subscription.query).toEqual({ key : 'value' });
expect(subscription.hash).toBe('hash');
expect(subscription.clientRequestIds.size).toBe(0);
});
it('can check it has subscribing clients', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
expect(subscription.hasSubscribingClient()).toBe(false);
});
it('can check it does not have subscribing clients', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
subscription.addClientSubscription(1, 1);
expect(subscription.hasSubscribingClient()).toBe(true);
});
it('can add one request for one client', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
subscription.addClientSubscription(1, 1);
expect(subscription.clientRequestIds.size).toBe(1);
expect(subscription.clientRequestIds.get(1)).toEqual([1]);
});
it('can add requests for one client', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
subscription.addClientSubscription(1, 1);
subscription.addClientSubscription(1, 2);
expect(subscription.clientRequestIds.size).toBe(1);
expect(subscription.clientRequestIds.get(1)).toEqual([1, 2]);
});
it('can add requests for clients', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
subscription.addClientSubscription(1, 1);
subscription.addClientSubscription(1, 2);
subscription.addClientSubscription(2, 2);
subscription.addClientSubscription(2, 3);
expect(subscription.clientRequestIds.size).toBe(2);
expect(subscription.clientRequestIds.get(1)).toEqual([1, 2]);
expect(subscription.clientRequestIds.get(2)).toEqual([2, 3]);
});
it('can delete requests for nonexistent client', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
subscription.deleteClientSubscription(1, 1);
var PLog =require('../src/LiveQuery/PLog');
expect(PLog.error).toHaveBeenCalled();
});
it('can delete nonexistent request for one client', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
subscription.addClientSubscription(1, 1);
subscription.deleteClientSubscription(1, 2);
var PLog =require('../src/LiveQuery/PLog');
expect(PLog.error).toHaveBeenCalled();
expect(subscription.clientRequestIds.size).toBe(1);
expect(subscription.clientRequestIds.get(1)).toEqual([1]);
});
it('can delete some requests for one client', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
subscription.addClientSubscription(1, 1);
subscription.addClientSubscription(1, 2);
subscription.deleteClientSubscription(1, 2);
var PLog =require('../src/LiveQuery/PLog');
expect(PLog.error).not.toHaveBeenCalled();
expect(subscription.clientRequestIds.size).toBe(1);
expect(subscription.clientRequestIds.get(1)).toEqual([1]);
});
it('can delete all requests for one client', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
subscription.addClientSubscription(1, 1);
subscription.addClientSubscription(1, 2);
subscription.deleteClientSubscription(1, 1);
subscription.deleteClientSubscription(1, 2);
var PLog =require('../src/LiveQuery/PLog');
expect(PLog.error).not.toHaveBeenCalled();
expect(subscription.clientRequestIds.size).toBe(0);
});
it('can delete requests for multiple clients', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
subscription.addClientSubscription(1, 1);
subscription.addClientSubscription(1, 2);
subscription.addClientSubscription(2, 1);
subscription.addClientSubscription(2, 2);
subscription.deleteClientSubscription(1, 2);
subscription.deleteClientSubscription(2, 1);
subscription.deleteClientSubscription(2, 2);
var PLog =require('../src/LiveQuery/PLog');
expect(PLog.error).not.toHaveBeenCalled();
expect(subscription.clientRequestIds.size).toBe(1);
expect(subscription.clientRequestIds.get(1)).toEqual([1]);
});
afterEach(function(){
jasmine.restoreLibrary('../src/LiveQuery/PLog', 'error');
});
});
+679
View File
@@ -0,0 +1,679 @@
"use strict";
var request = require('request');
var Config = require("../src/Config");
describe("Custom Pages Configuration", () => {
it("should set the custom pages", (done) => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
customPages: {
invalidLink: "myInvalidLink",
verifyEmailSuccess: "myVerifyEmailSuccess",
choosePassword: "myChoosePassword",
passwordResetSuccess: "myPasswordResetSuccess"
},
publicServerURL: "https://my.public.server.com/1"
});
var config = new Config("test");
expect(config.invalidLinkURL).toEqual("myInvalidLink");
expect(config.verifyEmailSuccessURL).toEqual("myVerifyEmailSuccess");
expect(config.choosePasswordURL).toEqual("myChoosePassword");
expect(config.passwordResetSuccessURL).toEqual("myPasswordResetSuccess");
expect(config.verifyEmailURL).toEqual("https://my.public.server.com/1/apps/test/verify_email");
expect(config.requestResetPasswordURL).toEqual("https://my.public.server.com/1/apps/test/request_password_reset");
done();
});
});
describe("Email Verification", () => {
it('sends verification email if email verification is enabled', done => {
var emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve()
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
});
spyOn(emailAdapter, 'sendVerificationEmail');
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.setEmail('cool_guy@parse.com');
user.signUp(null, {
success: function(user) {
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
user.fetch()
.then(() => {
expect(user.get('emailVerified')).toEqual(false);
done();
});
},
error: function(userAgain, error) {
fail('Failed to save user');
done();
}
});
});
it('does not send verification email when verification is enabled and email is not set', done => {
var emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve()
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
});
spyOn(emailAdapter, 'sendVerificationEmail');
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.signUp(null, {
success: function(user) {
expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
user.fetch()
.then(() => {
expect(user.get('emailVerified')).toEqual(undefined);
done();
});
},
error: function(userAgain, error) {
fail('Failed to save user');
done();
}
});
});
it('does send a validation email when updating the email', done => {
var emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve()
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
});
spyOn(emailAdapter, 'sendVerificationEmail');
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.signUp(null, {
success: function(user) {
expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
user.fetch()
.then((user) => {
user.set("email", "cool_guy@parse.com");
return user.save();
}).then((user) => {
return user.fetch();
}).then(() => {
expect(user.get('emailVerified')).toEqual(false);
// Wait as on update email, we need to fetch the username
setTimeout(function(){
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
done();
}, 200);
});
},
error: function(userAgain, error) {
fail('Failed to save user');
done();
}
});
});
it('does send a validation email with valid verification link when updating the email', done => {
var emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve()
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
});
spyOn(emailAdapter, 'sendVerificationEmail').and.callFake((options) => {
expect(options.link).not.toBeNull();
expect(options.link).not.toMatch(/token=undefined/);
Promise.resolve();
});
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.signUp(null, {
success: function(user) {
expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
user.fetch()
.then((user) => {
user.set("email", "cool_guy@parse.com");
return user.save();
}).then((user) => {
return user.fetch();
}).then(() => {
expect(user.get('emailVerified')).toEqual(false);
// Wait as on update email, we need to fetch the username
setTimeout(function(){
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
done();
}, 200);
});
},
error: function(userAgain, error) {
fail('Failed to save user');
done();
}
});
});
it('does send with a simple adapter', done => {
var calls = 0;
var emailAdapter = {
sendMail: function(options){
expect(options.to).toBe('cool_guy@parse.com');
if (calls == 0) {
expect(options.subject).toEqual('Please verify your e-mail for My Cool App');
expect(options.text.match(/verify_email/)).not.toBe(null);
} else if (calls == 1) {
expect(options.subject).toEqual('Password Reset for My Cool App');
expect(options.text.match(/request_password_reset/)).not.toBe(null);
}
calls++;
return Promise.resolve();
}
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'My Cool App',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
});
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.set("email", "cool_guy@parse.com");
user.signUp(null, {
success: function(user) {
expect(calls).toBe(1);
user.fetch()
.then((user) => {
return user.save();
}).then((user) => {
return Parse.User.requestPasswordReset("cool_guy@parse.com");
}).then(() => {
expect(calls).toBe(2);
done();
});
},
error: function(userAgain, error) {
fail('Failed to save user');
done();
}
});
});
it('does not send verification email if email verification is disabled', done => {
var emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve()
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: false,
emailAdapter: emailAdapter,
});
spyOn(emailAdapter, 'sendVerificationEmail');
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.signUp(null, {
success: function(user) {
user.fetch()
.then(() => {
expect(emailAdapter.sendVerificationEmail.calls.count()).toEqual(0);
expect(user.get('emailVerified')).toEqual(undefined);
done();
});
},
error: function(userAgain, error) {
fail('Failed to save user');
done();
}
});
});
it('receives the app name and user in the adapter', done => {
var emailAdapter = {
sendVerificationEmail: options => {
expect(options.appName).toEqual('emailing app');
expect(options.user.get('email')).toEqual('user@parse.com');
done();
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'emailing app',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
});
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.set('email', 'user@parse.com');
user.signUp(null, {
success: () => {},
error: function(userAgain, error) {
fail('Failed to save user');
done();
}
});
})
it('when you click the link in the email it sets emailVerified to true and redirects you', done => {
var user = new Parse.User();
var emailAdapter = {
sendVerificationEmail: options => {
request.get(options.link, {
followRedirect: false,
}, (error, response, body) => {
expect(response.statusCode).toEqual(302);
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user');
user.fetch()
.then(() => {
expect(user.get('emailVerified')).toEqual(true);
done();
}, (err) => {
console.error(err);
fail("this should not fail");
done();
});
});
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'emailing app',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
});
user.setPassword("asdf");
user.setUsername("user");
user.set('email', 'user@parse.com');
user.signUp();
});
it('redirects you to invalid link if you try to verify email incorrecly', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'emailing app',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
},
publicServerURL: "http://localhost:8378/1"
});
request.get('http://localhost:8378/1/apps/test/verify_email', {
followRedirect: false,
}, (error, response, body) => {
expect(response.statusCode).toEqual(302);
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
done()
});
});
it('redirects you to invalid link if you try to validate a nonexistant users email', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'emailing app',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
},
publicServerURL: "http://localhost:8378/1"
});
request.get('http://localhost:8378/1/apps/test/verify_email?token=asdfasdf&username=sadfasga', {
followRedirect: false,
}, (error, response, body) => {
expect(response.statusCode).toEqual(302);
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
done();
});
});
it('does not update email verified if you use an invalid token', done => {
var user = new Parse.User();
var emailAdapter = {
sendVerificationEmail: options => {
request.get('http://localhost:8378/1/apps/test/verify_email?token=invalid&username=zxcv', {
followRedirect: false,
}, (error, response, body) => {
expect(response.statusCode).toEqual(302);
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
user.fetch()
.then(() => {
expect(user.get('emailVerified')).toEqual(false);
done();
});
});
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'emailing app',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
});
user.setPassword("asdf");
user.setUsername("zxcv");
user.set('email', 'user@parse.com');
user.signUp(null, {
success: () => {},
error: function(userAgain, error) {
fail('Failed to save user');
done();
}
});
});
});
describe("Password Reset", () => {
it('should send a password reset link', done => {
var user = new Parse.User();
var emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: options => {
request.get(options.link, {
followRedirect: false,
}, (error, response, body) => {
if (error) {
console.error(error);
fail("Failed to get the reset link");
return;
}
expect(response.statusCode).toEqual(302);
var re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=zxcv%2Bzxcv/;
expect(response.body.match(re)).not.toBe(null);
done();
});
},
sendMail: () => {}
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'emailing app',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
});
user.setPassword("asdf");
user.setUsername("zxcv+zxcv");
user.set('email', 'user@parse.com');
user.signUp().then(() => {
Parse.User.requestPasswordReset('user@parse.com', {
error: (err) => {
console.error(err);
fail("Should not fail");
done();
}
});
});
});
it('redirects you to invalid link if you try to request password for a nonexistant users email', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'emailing app',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
},
publicServerURL: "http://localhost:8378/1"
});
request.get('http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf&username=sadfasga', {
followRedirect: false,
}, (error, response, body) => {
expect(response.statusCode).toEqual(302);
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
done();
});
});
it('should programatically reset password', done => {
var user = new Parse.User();
var emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: options => {
request.get(options.link, {
followRedirect: false,
}, (error, response, body) => {
if (error) {
console.error(error);
fail("Failed to get the reset link");
return;
}
expect(response.statusCode).toEqual(302);
var re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv/;
var match = response.body.match(re);
if (!match) {
fail("should have a token");
done();
return;
}
var token = match[1];
request.post({
url: "http://localhost:8378/1/apps/test/request_password_reset" ,
body: `new_password=hello&token=${token}&username=zxcv`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
followRedirect: false,
}, (error, response, body) => {
if (error) {
console.error(error);
fail("Failed to POST request password reset");
return;
}
expect(response.statusCode).toEqual(302);
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html');
Parse.User.logIn("zxcv", "hello").then(function(user){
let config = new Config('test');
config.database.adaptiveCollection('_User')
.then(coll => coll.find({ 'username': 'zxcv' }, { limit: 1 }))
.then((results) => {
// _perishable_token should be unset after reset password
expect(results.length).toEqual(1);
expect(results[0]['_perishable_token']).toEqual(undefined);
done();
});
}, (err) => {
console.error(err);
fail("should login with new password");
done();
});
});
});
},
sendMail: () => {}
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'emailing app',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
});
user.setPassword("asdf");
user.setUsername("zxcv");
user.set('email', 'user@parse.com');
user.signUp().then(() => {
Parse.User.requestPasswordReset('user@parse.com', {
error: (err) => {
console.error(err);
fail("Should not fail");
done();
}
});
});
});
})
+15 -2
View File
@@ -1,5 +1,3 @@
var Parse = require('parse/node').Parse;
Parse.Cloud.define('hello', function(req, res) {
res.success('Hello world!');
});
@@ -102,3 +100,18 @@ Parse.Cloud.define('requiredParameterCheck', function(req, res) {
}, function(params) {
return params.name;
});
Parse.Cloud.define('echoKeys', function(req, res){
return res.success({
applicationId: Parse.applicationId,
masterKey: Parse.masterKey,
javascriptKey: Parse.javascriptKey
})
});
Parse.Cloud.define('createBeforeSaveChangedObject', function(req, res){
var obj = new Parse.Object('BeforeSaveChanged');
obj.save().then(() => {
res.success(obj);
})
})
+6
View File
@@ -0,0 +1,6 @@
{
"arg1": "my_app",
"arg2": "8888",
"arg3": "hello",
"arg4": "/1"
}
+9
View File
@@ -0,0 +1,9 @@
{
"apps": [
{
"arg1": "my_app",
"arg2": 8888,
"arg3": "hello",
"arg4": "/1"
}]
}
+6
View File
@@ -0,0 +1,6 @@
{
"arg1": "my_app",
"arg2": "hello",
"arg3": "hello",
"arg4": "/1"
}
@@ -0,0 +1,16 @@
{
"apps": [
{
"arg1": "my_app",
"arg2": "99999",
"arg3": "hello",
"arg4": "/1"
},
{
"arg1": "my_app2",
"arg2": "9999",
"arg3": "hello",
"arg4": "/1"
}
]
}
+6
View File
@@ -0,0 +1,6 @@
{
"arg1": "my_app",
"arg2": "8888",
"arg3": "hello",
"myArg": "/1"
}
+20
View File
@@ -0,0 +1,20 @@
'use strict';
const request = require("request");
describe('features', () => {
it('requires the master key to get features', done => {
request.get({
url: 'http://localhost:8378/1/serverInfo',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(403);
expect(body.error).toEqual('unauthorized: master key is required');
done();
});
});
});
+65 -8
View File
@@ -2,19 +2,22 @@
jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000;
var cache = require('../src/cache');
var cache = require('../src/cache').default;
var DatabaseAdapter = require('../src/DatabaseAdapter');
var express = require('express');
var facebook = require('../src/oauth/facebook');
var facebook = require('../src/authDataManager/facebook');
var ParseServer = require('../src/index').ParseServer;
var path = require('path');
var databaseURI = process.env.DATABASE_URI;
var cloudMain = process.env.CLOUD_CODE_MAIN || './cloud/main.js';
var cloudMain = process.env.CLOUD_CODE_MAIN || './spec/cloud/main.js';
var port = 8378;
// Set up an API server for testing
var api = new ParseServer({
// Default server configuration for tests.
var defaultConfiguration = {
databaseURI: databaseURI,
cloud: cloudMain,
serverURL: 'http://localhost:' + port + '/1',
appId: 'test',
javascriptKey: 'test',
dotNetKey: 'windows',
@@ -23,19 +26,50 @@ var api = new ParseServer({
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
push: {
'ios': {
cert: 'prodCert.pem',
key: 'prodKey.pem',
production: true,
bundleId: 'bundleId'
}
},
oauth: { // Override the facebook provider
facebook: mockFacebook(),
myoauth: {
module: "../spec/myoauth" // relative path as it's run from src
module: path.resolve(__dirname, "myoauth") // relative path as it's run from src
}
}
});
};
// Set up a default API server for testing with default configuration.
var api = new ParseServer(defaultConfiguration);
var app = express();
app.use('/1', api);
var port = 8378;
var server = app.listen(port);
// Prevent reinitializing the server from clobbering Cloud Code
delete defaultConfiguration.cloud;
var currentConfiguration;
// Allows testing specific configurations of Parse Server
var setServerConfiguration = configuration => {
// the configuration hasn't changed
if (configuration === currentConfiguration) {
return;
}
DatabaseAdapter.clearDatabaseSettings();
currentConfiguration = configuration;
server.close();
cache.clearCache();
app = express();
api = new ParseServer(configuration);
app.use('/1', api);
server = app.listen(port);
};
var restoreServerConfiguration = () => setServerConfiguration(defaultConfiguration);
// Set up a Parse client to talk to our test API server
var Parse = require('parse/node');
Parse.serverURL = 'http://localhost:' + port + '/1';
@@ -45,7 +79,9 @@ Parse.serverURL = 'http://localhost:' + port + '/1';
Parse.Promise.disableAPlusCompliant();
beforeEach(function(done) {
restoreServerConfiguration();
Parse.initialize('test', 'test', 'test');
Parse.serverURL = 'http://localhost:' + port + '/1';
Parse.User.enableUnsafeCurrentUser();
done();
});
@@ -221,3 +257,24 @@ global.expectError = expectError;
global.arrayContains = arrayContains;
global.jequal = jequal;
global.range = range;
global.setServerConfiguration = setServerConfiguration;
global.defaultConfiguration = defaultConfiguration;
// LiveQuery test setting
require('../src/LiveQuery/PLog').logLevel = 'NONE';
var libraryCache = {};
jasmine.mockLibrary = function(library, name, mock) {
var original = require(library)[name];
if (!libraryCache[library]) {
libraryCache[library] = {};
}
require(library)[name] = mock;
libraryCache[library][name] = original;
}
jasmine.restoreLibrary = function(library, name) {
if (!libraryCache[library] || !libraryCache[library][name]) {
throw 'Can not find library ' + library + ' ' + name;
}
require(library)[name] = libraryCache[library][name];
}
+336
View File
@@ -0,0 +1,336 @@
var request = require('request');
var parseServerPackage = require('../package.json');
var MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions');
var ParseServer = require("../src/index");
var Config = require('../src/Config');
var express = require('express');
describe('server', () => {
it('requires a master key and app id', done => {
expect(setServerConfiguration.bind(undefined, { })).toThrow('You must provide an appId!');
expect(setServerConfiguration.bind(undefined, { appId: 'myId' })).toThrow('You must provide a masterKey!');
expect(setServerConfiguration.bind(undefined, { appId: 'myId', masterKey: 'mk' })).toThrow('You must provide a serverURL!');
done();
});
it('fails if database is unreachable', done => {
setServerConfiguration({
databaseURI: 'mongodb://fake:fake@ds043605.mongolab.com:43605/drew3',
serverURL: 'http://localhost:8378/1',
appId: 'test',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
});
//Need to use rest api because saving via JS SDK results in fail() not getting called
request.post({
url: 'http://localhost:8378/1/classes/NewClass',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
body: {},
json: true,
}, (error, response, body) => {
expect(response.statusCode).toEqual(500);
expect(body.code).toEqual(1);
expect(body.message).toEqual('Internal server error.');
done();
});
});
it('can load email adapter via object', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: MockEmailAdapterWithOptions({
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
}),
publicServerURL: 'http://localhost:8378/1'
});
done();
});
it('can load email adapter via class', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: {
class: MockEmailAdapterWithOptions,
options: {
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
}
},
publicServerURL: 'http://localhost:8378/1'
});
done();
});
it('can load email adapter via module name', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: {
module: 'parse-server-simple-mailgun-adapter',
options: {
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
}
},
publicServerURL: 'http://localhost:8378/1'
});
done();
});
it('can load email adapter via only module name', done => {
expect(() => setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: 'parse-server-simple-mailgun-adapter',
publicServerURL: 'http://localhost:8378/1'
})).toThrow('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.');
done();
});
it('throws if you initialize email adapter incorrecly', done => {
expect(() => setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: {
module: 'parse-server-simple-mailgun-adapter',
options: {
domain: 'd',
}
},
publicServerURL: 'http://localhost:8378/1'
})).toThrow('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.');
done();
});
it('can report the server version', done => {
request.get({
url: 'http://localhost:8378/1/serverInfo',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
json: true,
}, (error, response, body) => {
expect(body.parseServerVersion).toEqual(parseServerPackage.version);
done();
})
});
it('can load absolute cloud code file', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
masterKey: 'test',
cloud: __dirname + '/cloud/main.js'
});
done();
});
it('can load relative cloud code file', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
masterKey: 'test',
cloud: './spec/cloud/main.js'
});
done();
});
it('can create a parse-server', done => {
var parseServer = new ParseServer.default({
appId: "aTestApp",
masterKey: "aTestMasterKey",
serverURL: "http://localhost:12666/parse",
databaseURI: 'mongodb://localhost:27017/aTestApp'
});
expect(Parse.applicationId).toEqual("aTestApp");
var app = express();
app.use('/parse', parseServer.app);
var server = app.listen(12666);
var obj = new Parse.Object("AnObject");
var objId;
obj.save().then((obj) => {
objId = obj.id;
var q = new Parse.Query("AnObject");
return q.first();
}).then((obj) => {
expect(obj.id).toEqual(objId);
server.close();
done();
}).fail((err) => {
server.close();
done();
})
});
it('can create a parse-server', done => {
var parseServer = ParseServer.ParseServer({
appId: "anOtherTestApp",
masterKey: "anOtherTestMasterKey",
serverURL: "http://localhost:12667/parse",
databaseURI: 'mongodb://localhost:27017/anotherTstApp'
});
expect(Parse.applicationId).toEqual("anOtherTestApp");
var app = express();
app.use('/parse', parseServer);
var server = app.listen(12667);
var obj = new Parse.Object("AnObject");
var objId;
obj.save().then((obj) => {
objId = obj.id;
var q = new Parse.Query("AnObject");
return q.first();
}).then((obj) => {
expect(obj.id).toEqual(objId);
server.close();
done();
}).fail((err) => {
server.close();
done();
})
});
it('has createLiveQueryServer', done => {
// original implementation through the factory
expect(typeof ParseServer.ParseServer.createLiveQueryServer).toEqual('function');
// For import calls
expect(typeof ParseServer.default.createLiveQueryServer).toEqual('function');
done();
});
it('exposes all the "core" adapters', done => {
expect(ParseServer.S3Adapter).toThrow();
expect(ParseServer.GCSAdapter).toThrow('GCSAdapter requires an projectId');
expect(ParseServer.FileSystemAdapter).toThrow();
done();
});
it('properly gives publicServerURL when set', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
masterKey: 'test',
publicServerURL: 'https://myserver.com/1'
});
var config = new Config('test', 'http://localhost:8378/1');
expect(config.mount).toEqual('https://myserver.com/1');
done();
});
it('properly removes trailing slash in mount', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
masterKey: 'test'
});
var config = new Config('test', 'http://localhost:8378/1/');
expect(config.mount).toEqual('http://localhost:8378/1');
done();
});
it('should throw when getting invalid mount', done => {
expect(() => setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
masterKey: 'test',
publicServerURL: 'blabla:/some'
}) ).toThrow("publicServerURL should be a valid HTTPS URL starting with https://");
done();
});
it('fails if the session length is not a number', (done) => {
expect(() => setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
masterKey: 'test',
sessionLength: 'test'
})).toThrow('Session length must be a valid number.');
done();
});
it('fails if the session length is less than or equal to 0', (done) => {
expect(() => setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
masterKey: 'test',
sessionLength: '-33'
})).toThrow('Session length must be a value greater than 0.');
expect(() => setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
masterKey: 'test',
sessionLength: '0'
})).toThrow('Session length must be a value greater than 0.');
done();
})
});
+916 -22
View File
File diff suppressed because it is too large Load Diff
-208
View File
@@ -1,208 +0,0 @@
"use strict";
const Parse = require('parse/node').Parse;
// TODO: apn does not support the new HTTP/2 protocal. It is fine to use it in V1,
// but probably we will replace it in the future.
const apn = require('apn');
/**
* Create a new connection to the APN service.
* @constructor
* @param {Object|Array} args An argument or a list of arguments to config APNS connection
* @param {String} args.cert The filename of the connection certificate to load from disk
* @param {String} args.key The filename of the connection key to load from disk
* @param {String} args.pfx The filename for private key, certificate and CA certs in PFX or PKCS12 format, it will overwrite cert and key
* @param {String} args.passphrase The passphrase for the connection key, if required
* @param {String} args.bundleId The bundleId for cert
* @param {Boolean} args.production Specifies which environment to connect to: Production (if true) or Sandbox
*/
function APNS(args) {
// Since for ios, there maybe multiple cert/key pairs,
// typePushConfig can be an array.
let apnsArgsList = [];
if (Array.isArray(args)) {
apnsArgsList = apnsArgsList.concat(args);
} else if (typeof args === 'object') {
apnsArgsList.push(args);
} else {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'APNS Configuration is invalid');
}
this.conns = [];
for (let apnsArgs of apnsArgsList) {
let conn = new apn.Connection(apnsArgs);
if (!apnsArgs.bundleId) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'BundleId is mssing for %j', apnsArgs);
}
conn.bundleId = apnsArgs.bundleId;
// Set the priority of the conns, prod cert has higher priority
if (apnsArgs.production) {
conn.priority = 0;
} else {
conn.priority = 1;
}
// Set apns client callbacks
conn.on('connected', () => {
console.log('APNS Connection %d Connected', conn.index);
});
conn.on('transmissionError', (errCode, notification, apnDevice) => {
handleTransmissionError(this.conns, errCode, notification, apnDevice);
});
conn.on('timeout', () => {
console.log('APNS Connection %d Timeout', conn.index);
});
conn.on('disconnected', () => {
console.log('APNS Connection %d Disconnected', conn.index);
});
conn.on('socketError', () => {
console.log('APNS Connection %d Socket Error', conn.index);
});
conn.on('transmitted', function(notification, device) {
console.log('APNS Connection %d Notification transmitted to %s', conn.index, device.token.toString('hex'));
});
this.conns.push(conn);
}
// Sort the conn based on priority ascending, high pri first
this.conns.sort((s1, s2) => {
return s1.priority - s2.priority;
});
// Set index of conns
for (let index = 0; index < this.conns.length; index++) {
this.conns[index].index = index;
}
}
/**
* Send apns request.
* @param {Object} data The data we need to send, the format is the same with api request body
* @param {Array} devices A array of devices
* @returns {Object} A promise which is resolved immediately
*/
APNS.prototype.send = function(data, devices) {
let coreData = data.data;
let expirationTime = data['expiration_time'];
let notification = generateNotification(coreData, expirationTime);
for (let device of devices) {
let qualifiedConnIndexs = chooseConns(this.conns, device);
// We can not find a valid conn, just ignore this device
if (qualifiedConnIndexs.length == 0) {
continue;
}
let conn = this.conns[qualifiedConnIndexs[0]];
let apnDevice = new apn.Device(device.deviceToken);
apnDevice.connIndex = qualifiedConnIndexs[0];
// Add additional appIdentifier info to apn device instance
if (device.appIdentifier) {
apnDevice.appIdentifier = device.appIdentifier;
}
conn.pushNotification(notification, apnDevice);
}
return Parse.Promise.as();
}
function handleTransmissionError(conns, errCode, notification, apnDevice) {
console.error('APNS Notification caused error: ' + errCode + ' for device ', apnDevice, notification);
// This means the error notification is not in the cache anymore or the recepient is missing,
// we just ignore this case
if (!notification || !apnDevice) {
return
}
// If currentConn can not send the push notification, we try to use the next available conn.
// Since conns is sorted by priority, the next conn means the next low pri conn.
// If there is no conn available, we give up on sending the notification to that device.
let qualifiedConnIndexs = chooseConns(conns, apnDevice);
let currentConnIndex = apnDevice.connIndex;
let newConnIndex = -1;
// Find the next element of currentConnIndex in qualifiedConnIndexs
for (let index = 0; index < qualifiedConnIndexs.length - 1; index++) {
if (qualifiedConnIndexs[index] === currentConnIndex) {
newConnIndex = qualifiedConnIndexs[index + 1];
break;
}
}
// There is no more available conns, we give up in this case
if (newConnIndex < 0 || newConnIndex >= conns.length) {
console.log('APNS can not find vaild connection for %j', apnDevice.token);
return;
}
let newConn = conns[newConnIndex];
// Update device conn info
apnDevice.connIndex = newConnIndex;
// Use the new conn to send the notification
newConn.pushNotification(notification, apnDevice);
}
function chooseConns(conns, device) {
// If device does not have appIdentifier, all conns maybe proper connections.
// Otherwise we try to match the appIdentifier with bundleId
let qualifiedConns = [];
for (let index = 0; index < conns.length; index++) {
let conn = conns[index];
// If the device we need to send to does not have
// appIdentifier, any conn could be a qualified connection
if (!device.appIdentifier || device.appIdentifier === '') {
qualifiedConns.push(index);
continue;
}
if (device.appIdentifier === conn.bundleId) {
qualifiedConns.push(index);
}
}
return qualifiedConns;
}
/**
* Generate the apns notification from the data we get from api request.
* @param {Object} coreData The data field under api request body
* @returns {Object} A apns notification
*/
function generateNotification(coreData, expirationTime) {
let notification = new apn.notification();
let payload = {};
for (let key in coreData) {
switch (key) {
case 'alert':
notification.setAlertText(coreData.alert);
break;
case 'badge':
notification.badge = coreData.badge;
break;
case 'sound':
notification.sound = coreData.sound;
break;
case 'content-available':
notification.setNewsstandAvailable(true);
let isAvailable = coreData['content-available'] === 1;
notification.setContentAvailable(isAvailable);
break;
case 'category':
notification.category = coreData.category;
break;
default:
payload[key] = coreData[key];
break;
}
}
notification.payload = payload;
notification.expiry = expirationTime;
return notification;
}
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
APNS.generateNotification = generateNotification;
APNS.chooseConns = chooseConns;
APNS.handleTransmissionError = handleTransmissionError;
}
module.exports = APNS;
+38
View File
@@ -0,0 +1,38 @@
export function loadAdapter(adapter, defaultAdapter, options) {
if (!adapter)
{
if (!defaultAdapter) {
return options;
}
// Load from the default adapter when no adapter is set
return loadAdapter(defaultAdapter, undefined, options);
} else if (typeof adapter === "function") {
try {
return adapter(options);
} catch(e) {
if (e.name === 'TypeError') {
var Adapter = adapter;
return new Adapter(options);
} else {
throw e;
}
}
} else if (typeof adapter === "string") {
adapter = require(adapter);
// If it's define as a module, get the default
if (adapter.default) {
adapter = adapter.default;
}
return loadAdapter(adapter, undefined, options);
} else if (adapter.module) {
return loadAdapter(adapter.module, undefined, adapter.options);
} else if (adapter.class) {
return loadAdapter(adapter.class, undefined, adapter.options);
} else if (adapter.adapter) {
return loadAdapter(adapter.adapter, undefined, adapter.options);
}
// return the adapter as provided
return adapter;
}
export default loadAdapter;
+23
View File
@@ -0,0 +1,23 @@
/*
Mail Adapter prototype
A MailAdapter should implement at least sendMail()
*/
export class MailAdapter {
/*
* A method for sending mail
* @param options would have the parameters
* - to: the recipient
* - text: the raw text of the message
* - subject: the subject of the email
*/
sendMail(options) {}
/* You can implement those methods if you want
* to provide HTML templates etc...
*/
// sendVerificationEmail({ link, appName, user }) {}
// sendPasswordResetEmail({ link, appName, user }) {}
}
export default MailAdapter;
+14 -4
View File
@@ -8,15 +8,25 @@
// * getFileLocation(config, request, filename)
//
// Default is GridStoreAdapter, which requires mongo
// and for the API server to be using the ExportAdapter
// and for the API server to be using the DatabaseController with Mongo
// database adapter.
export class FilesAdapter {
createFile(config, filename, data) { }
/* this method is responsible to store the file in order to be retrived later by it's file name
*
* @param filename the filename to save
* @param data the buffer of data from the file
* @param contentType the supposed contentType
* @discussion the contentType can be undefined if the controller was not able to determine it
*
* @return a promise that should fail if the storage didn't succeed
*
*/
createFile(filename: string, data, contentType: string) { }
deleteFile(config, filename) { }
deleteFile(filename) { }
getFileData(config, filename) { }
getFileData(filename) { }
getFileLocation(config, filename) { }
}
+40 -20
View File
@@ -1,28 +1,47 @@
// GridStoreAdapter
//
// Stores files in Mongo using GridStore
// Requires the database adapter to be based on mongoclient
/**
GridStoreAdapter
Stores files in Mongo using GridStore
Requires the database adapter to be based on mongoclient
import { GridStore } from 'mongodb';
@flow weak
*/
import { MongoClient, GridStore, Db} from 'mongodb';
import { FilesAdapter } from './FilesAdapter';
export class GridStoreAdapter extends FilesAdapter {
_databaseURI: string;
_connectionPromise: Promise<Db>;
constructor(mongoDatabaseURI: string) {
super();
this._databaseURI = mongoDatabaseURI;
this._connect();
}
_connect() {
if (!this._connectionPromise) {
this._connectionPromise = MongoClient.connect(this._databaseURI);
}
return this._connectionPromise;
}
// For a given config object, filename, and data, store a file
// Returns a promise
createFile(config, filename, data) {
return config.database.connect().then(() => {
let gridStore = new GridStore(config.database.db, filename, 'w');
createFile(filename: string, data, contentType) {
return this._connect().then(database => {
let gridStore = new GridStore(database, filename, 'w');
return gridStore.open();
}).then((gridStore) => {
}).then(gridStore => {
return gridStore.write(data);
}).then((gridStore) => {
}).then(gridStore => {
return gridStore.close();
});
}
deleteFile(config, filename) {
return config.database.connect().then(() => {
let gridStore = new GridStore(config.database.db, filename, 'w');
deleteFile(filename: string) {
return this._connect().then(database => {
let gridStore = new GridStore(database, filename, 'w');
return gridStore.open();
}).then((gridStore) => {
return gridStore.unlink();
@@ -31,13 +50,14 @@ export class GridStoreAdapter extends FilesAdapter {
});
}
getFileData(config, filename) {
return config.database.connect().then(() => {
return GridStore.exist(config.database.db, filename);
}).then(() => {
let gridStore = new GridStore(config.database.db, filename, 'r');
return gridStore.open();
}).then((gridStore) => {
getFileData(filename: string) {
return this._connect().then(database => {
return GridStore.exist(database, filename)
.then(() => {
let gridStore = new GridStore(database, filename, 'r');
return gridStore.open();
});
}).then(gridStore => {
return gridStore.read();
});
}
-97
View File
@@ -1,97 +0,0 @@
// S3Adapter
//
// Stores Parse files in AWS S3.
import * as AWS from 'aws-sdk';
import { FilesAdapter } from './FilesAdapter';
const DEFAULT_S3_REGION = "us-east-1";
const DEFAULT_S3_BUCKET = "parse-files";
export class S3Adapter extends FilesAdapter {
// Creates an S3 session.
// Providing AWS access and secret keys is mandatory
// Region and bucket will use sane defaults if omitted
constructor(
accessKey,
secretKey,
{ region = DEFAULT_S3_REGION,
bucket = DEFAULT_S3_BUCKET,
bucketPrefix = '',
directAccess = false } = {}
) {
super();
this._region = region;
this._bucket = bucket;
this._bucketPrefix = bucketPrefix;
this._directAccess = directAccess;
let s3Options = {
accessKeyId: accessKey,
secretAccessKey: secretKey,
params: { Bucket: this._bucket }
};
AWS.config._region = this._region;
this._s3Client = new AWS.S3(s3Options);
}
// For a given config object, filename, and data, store a file in S3
// Returns a promise containing the S3 object creation response
createFile(config, filename, data) {
let params = {
Key: this._bucketPrefix + filename,
Body: data
};
if (this._directAccess) {
params.ACL = "public-read"
}
return new Promise((resolve, reject) => {
this._s3Client.upload(params, (err, data) => {
if (err !== null) {
return reject(err);
}
resolve(data);
});
});
}
deleteFile(config, filename) {
return new Promise((resolve, reject) => {
let params = {
Key: this._bucketPrefix + filename
};
this._s3Client.deleteObject(params, (err, data) =>{
if(err !== null) {
return reject(err);
}
resolve(data);
});
});
}
// Search for and return a file if found by filename
// Returns a promise that succeeds with the buffer result from S3
getFileData(config, filename) {
let params = {Key: this._bucketPrefix + filename};
return new Promise((resolve, reject) => {
this._s3Client.getObject(params, (err, data) => {
if (err !== null) {
return reject(err);
}
resolve(data.Body);
});
});
}
// Generates and returns the location of a file stored in S3 for the given request and filename
// The location is the direct S3 link if the option is set, otherwise we serve the file through parse-server
getFileLocation(config, filename) {
if (this._directAccess) {
return `https://${this._bucket}.s3.amazonaws.com/${this._bucketPrefix + filename}`;
}
return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename));
}
}
export default S3Adapter;
+24 -130
View File
@@ -1,7 +1,7 @@
// Logger
//
// Wrapper around Winston logging library with custom query
//
//
// expected log entry to be in the shape of:
// {"level":"info","message":"Your Message","timestamp":"2016-02-04T05:59:27.412Z"}
//
@@ -9,6 +9,7 @@ import { LoggerAdapter } from './LoggerAdapter';
import winston from 'winston';
import fs from 'fs';
import { Parse } from 'parse/node';
import { logger, configure } from '../../logger';
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
const CACHE_TIME = 1000 * 60;
@@ -57,24 +58,6 @@ let _hasValidCache = (from, until, level) => {
return false;
}
// renews transports to current date
let _renewTransports = ({infoLogger, errorLogger, logsFolder}) => {
if (infoLogger) {
infoLogger.add(winston.transports.File, {
filename: logsFolder + _getFileName() + '.info',
name: 'info-file',
level: 'info'
});
}
if (errorLogger) {
errorLogger.add(winston.transports.File, {
filename: logsFolder + _getFileName() + '.error',
name: 'error-file',
level: 'error'
});
}
};
// check that log entry has valid time stamp based on query
let _isValidLogEntry = (from, until, entry) => {
var _entry = JSON.parse(entry),
@@ -84,140 +67,51 @@ let _isValidLogEntry = (from, until, entry) => {
: false
};
// ensure that file name is up to date
let _verifyTransports = ({infoLogger, errorLogger, logsFolder}) => {
if (_getNearestDay(currentDate) !== _getNearestDay(new Date())) {
currentDate = new Date();
if (infoLogger) {
infoLogger.remove('info-file');
}
if (errorLogger) {
errorLogger.remove('error-file');
}
_renewTransports({infoLogger, errorLogger, logsFolder});
}
}
export class FileLoggerAdapter extends LoggerAdapter {
constructor(options = {}) {
super();
this._logsFolder = options.logsFolder || LOGS_FOLDER;
// check logs folder exists
if (!fs.existsSync(this._logsFolder)) {
fs.mkdirSync(this._logsFolder);
}
this._errorLogger = new (winston.Logger)({
exitOnError: false,
transports: [
new (winston.transports.File)({
filename: this._logsFolder + _getFileName() + '.error',
name: 'error-file',
level: 'error'
})
]
});
this._infoLogger = new (winston.Logger)({
exitOnError: false,
transports: [
new (winston.transports.File)({
filename: this._logsFolder + _getFileName() + '.info',
name: 'info-file',
level: 'info'
})
]
});
}
info() {
_verifyTransports({infoLogger: this._infoLogger, logsFolder: this._logsFolder});
return this._infoLogger.info.apply(undefined, arguments);
return logger.info.apply(undefined, arguments);
}
error() {
_verifyTransports({errorLogger: this._errorLogger, logsFolder: this._logsFolder});
return this._errorLogger.error.apply(undefined, arguments);
return logger.error.apply(undefined, arguments);
}
// custom query as winston is currently limited
query(options, callback) {
query(options, callback = () => {}) {
if (!options) {
options = {};
}
// defaults to 7 days prior
let from = options.from || new Date(Date.now() - (7 * MILLISECONDS_IN_A_DAY));
let until = options.until || new Date();
let size = options.size || 10;
let limit = options.size || 10;
let order = options.order || 'desc';
let level = options.level || 'info';
let roundedUntil = _getNearestDay(until);
let roundedFrom = _getNearestDay(from);
if (_hasValidCache(roundedFrom, roundedUntil, level)) {
let logs = [];
if (order !== simpleCache.order) {
// reverse order of data
simpleCache.data.forEach((entry) => {
logs.unshift(entry);
});
} else {
logs = simpleCache.data;
}
callback(logs.slice(0, size));
return;
}
var options = {
from,
until,
limit,
order
};
let curDate = roundedUntil;
let curSize = 0;
let method = order === 'desc' ? 'push' : 'unshift';
let files = [];
let promises = [];
// current a batch call, all files with valid dates are read
while (curDate >= from) {
files[method](this._logsFolder + curDate.toISOString() + '.' + level);
curDate = _getPrevDay(curDate);
}
// read each file and split based on newline char.
// limitation is message cannot contain newline
// TODO: strip out delimiter from logged message
files.forEach(function(file, i) {
let promise = new Parse.Promise();
fs.readFile(file, 'utf8', function(err, data) {
return new Promise((resolve, reject) => {
logger.query(options, (err, res) => {
if (err) {
promise.resolve([]);
} else {
let results = data.split('\n').filter((value) => {
return value.trim() !== '';
});
promise.resolve(results);
callback(err);
return reject(err);
}
});
promises[method](promise);
});
Parse.Promise.when(promises).then((results) => {
let logs = [];
results.forEach(function(logEntries, i) {
logEntries.forEach(function(entry) {
if (_isValidLogEntry(from, until, entry)) {
logs[method](JSON.parse(entry));
}
});
});
simpleCache = {
timestamp: new Date(),
from: roundedFrom,
until: roundedUntil,
data: logs,
order,
level,
};
callback(logs.slice(0, size));
if (level == 'error') {
callback(res['parse-server-error']);
resolve(res['parse-server-error']);
} else {
callback(res['parse-server']);
resolve(res['parse-server']);
}
})
});
}
}
-230
View File
@@ -1,230 +0,0 @@
"use strict";
// ParsePushAdapter is the default implementation of
// PushAdapter, it uses GCM for android push and APNS
// for ios push.
const Parse = require('parse/node').Parse;
var deepcopy = require('deepcopy');
function OneSignalPushAdapter(pushConfig) {
this.https = require('https');
this.validPushTypes = ['ios', 'android'];
this.senderMap = {};
pushConfig = pushConfig || {};
this.OneSignalConfig = {};
this.OneSignalConfig['appId'] = pushConfig['oneSignalAppId'];
this.OneSignalConfig['apiKey'] = pushConfig['oneSignalApiKey'];
this.senderMap['ios'] = this.sendToAPNS.bind(this);
this.senderMap['android'] = this.sendToGCM.bind(this);
}
/**
* Get an array of valid push types.
* @returns {Array} An array of valid push types
*/
OneSignalPushAdapter.prototype.getValidPushTypes = function() {
return this.validPushTypes;
}
OneSignalPushAdapter.prototype.send = function(data, installations) {
console.log("Sending notification to "+installations.length+" devices.")
let deviceMap = classifyInstallation(installations, this.validPushTypes);
let sendPromises = [];
for (let pushType in deviceMap) {
let sender = this.senderMap[pushType];
if (!sender) {
console.log('Can not find sender for push type %s, %j', pushType, data);
continue;
}
let devices = deviceMap[pushType];
if(devices.length > 0) {
sendPromises.push(sender(data, devices));
}
}
return Parse.Promise.when(sendPromises);
}
OneSignalPushAdapter.prototype.sendToAPNS = function(data,tokens) {
data= deepcopy(data['data']);
var post = {};
if(data['badge']) {
if(data['badge'] == "Increment") {
post['ios_badgeType'] = 'Increase';
post['ios_badgeCount'] = 1;
} else {
post['ios_badgeType'] = 'SetTo';
post['ios_badgeCount'] = data['badge'];
}
delete data['badge'];
}
if(data['alert']) {
post['contents'] = {en: data['alert']};
delete data['alert'];
}
if(data['sound']) {
post['ios_sound'] = data['sound'];
delete data['sound'];
}
if(data['content-available'] == 1) {
post['content_available'] = true;
delete data['content-available'];
}
post['data'] = data;
let promise = new Parse.Promise();
var chunk = 2000 // OneSignal can process 2000 devices at a time
var tokenlength=tokens.length;
var offset = 0
// handle onesignal response. Start next batch if there's not an error.
let handleResponse = function(wasSuccessful) {
if (!wasSuccessful) {
return promise.reject("OneSignal Error");
}
if(offset >= tokenlength) {
promise.resolve()
} else {
this.sendNext();
}
}.bind(this)
this.sendNext = function() {
post['include_ios_tokens'] = [];
tokens.slice(offset,offset+chunk).forEach(function(i) {
post['include_ios_tokens'].push(i['deviceToken'])
})
offset+=chunk;
this.sendToOneSignal(post, handleResponse);
}.bind(this)
this.sendNext()
return promise;
}
OneSignalPushAdapter.prototype.sendToGCM = function(data,tokens) {
data= deepcopy(data['data']);
var post = {};
if(data['alert']) {
post['contents'] = {en: data['alert']};
delete data['alert'];
}
if(data['title']) {
post['title'] = {en: data['title']};
delete data['title'];
}
if(data['uri']) {
post['url'] = data['uri'];
}
post['data'] = data;
let promise = new Parse.Promise();
var chunk = 2000 // OneSignal can process 2000 devices at a time
var tokenlength=tokens.length;
var offset = 0
// handle onesignal response. Start next batch if there's not an error.
let handleResponse = function(wasSuccessful) {
if (!wasSuccessful) {
return promise.reject("OneSIgnal Error");
}
if(offset >= tokenlength) {
promise.resolve()
} else {
this.sendNext();
}
}.bind(this);
this.sendNext = function() {
post['include_android_reg_ids'] = [];
tokens.slice(offset,offset+chunk).forEach(function(i) {
post['include_android_reg_ids'].push(i['deviceToken'])
})
offset+=chunk;
this.sendToOneSignal(post, handleResponse);
}.bind(this)
this.sendNext();
return promise;
}
OneSignalPushAdapter.prototype.sendToOneSignal = function(data, cb) {
let headers = {
"Content-Type": "application/json",
"Authorization": "Basic "+this.OneSignalConfig['apiKey']
};
let options = {
host: "onesignal.com",
port: 443,
path: "/api/v1/notifications",
method: "POST",
headers: headers
};
data['app_id'] = this.OneSignalConfig['appId'];
let request = this.https.request(options, function(res) {
if(res.statusCode < 299) {
cb(true);
} else {
console.log('OneSignal Error');
res.on('data', function(chunk) {
console.log(chunk.toString())
});
cb(false)
}
});
request.on('error', function(e) {
console.log("Error connecting to OneSignal")
console.log(e);
cb(false);
});
request.write(JSON.stringify(data))
request.end();
}
/**g
* Classify the device token of installations based on its device type.
* @param {Object} installations An array of installations
* @param {Array} validPushTypes An array of valid push types(string)
* @returns {Object} A map whose key is device type and value is an array of device
*/
function classifyInstallation(installations, validPushTypes) {
// Init deviceTokenMap, create a empty array for each valid pushType
let deviceMap = {};
for (let validPushType of validPushTypes) {
deviceMap[validPushType] = [];
}
for (let installation of installations) {
// No deviceToken, ignore
if (!installation.deviceToken) {
continue;
}
let pushType = installation.deviceType;
if (deviceMap[pushType]) {
deviceMap[pushType].push({
deviceToken: installation.deviceToken
});
} else {
console.log('Unknown push type from installation %j', installation);
}
}
return deviceMap;
}
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
OneSignalPushAdapter.classifyInstallation = classifyInstallation;
}
module.exports = OneSignalPushAdapter;
-88
View File
@@ -1,88 +0,0 @@
"use strict";
// ParsePushAdapter is the default implementation of
// PushAdapter, it uses GCM for android push and APNS
// for ios push.
const Parse = require('parse/node').Parse;
const GCM = require('../../GCM');
const APNS = require('../../APNS');
function ParsePushAdapter(pushConfig) {
this.validPushTypes = ['ios', 'android'];
this.senderMap = {};
pushConfig = pushConfig || {};
let pushTypes = Object.keys(pushConfig);
for (let pushType of pushTypes) {
if (this.validPushTypes.indexOf(pushType) < 0) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Push to ' + pushTypes + ' is not supported');
}
switch (pushType) {
case 'ios':
this.senderMap[pushType] = new APNS(pushConfig[pushType]);
break;
case 'android':
this.senderMap[pushType] = new GCM(pushConfig[pushType]);
break;
}
}
}
/**
* Get an array of valid push types.
* @returns {Array} An array of valid push types
*/
ParsePushAdapter.prototype.getValidPushTypes = function() {
return this.validPushTypes;
}
ParsePushAdapter.prototype.send = function(data, installations) {
let deviceMap = classifyInstallation(installations, this.validPushTypes);
let sendPromises = [];
for (let pushType in deviceMap) {
let sender = this.senderMap[pushType];
if (!sender) {
console.log('Can not find sender for push type %s, %j', pushType, data);
continue;
}
let devices = deviceMap[pushType];
sendPromises.push(sender.send(data, devices));
}
return Parse.Promise.when(sendPromises);
}
/**g
* Classify the device token of installations based on its device type.
* @param {Object} installations An array of installations
* @param {Array} validPushTypes An array of valid push types(string)
* @returns {Object} A map whose key is device type and value is an array of device
*/
function classifyInstallation(installations, validPushTypes) {
// Init deviceTokenMap, create a empty array for each valid pushType
let deviceMap = {};
for (let validPushType of validPushTypes) {
deviceMap[validPushType] = [];
}
for (let installation of installations) {
// No deviceToken, ignore
if (!installation.deviceToken) {
continue;
}
let pushType = installation.deviceType;
if (deviceMap[pushType]) {
deviceMap[pushType].push({
deviceToken: installation.deviceToken,
appIdentifier: installation.appIdentifier
});
} else {
console.log('Unknown push type from installation %j', installation);
}
}
return deviceMap;
}
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
ParsePushAdapter.classifyInstallation = classifyInstallation;
}
module.exports = ParsePushAdapter;
+9 -4
View File
@@ -4,14 +4,19 @@
//
// Adapter classes must implement the following functions:
// * getValidPushTypes()
// * send(devices, installations)
// * send(devices, installations, pushStatus)
//
// Default is ParsePushAdapter, which uses GCM for
// android push and APNS for ios push.
export class PushAdapter {
send(devices, installations) { }
getValidPushTypes() { }
export class PushAdapter {
send(devices, installations, pushStatus) { }
/**
* Get an array of valid push types.
* @returns {Array} An array of valid push types
*/
getValidPushTypes() {}
}
export default PushAdapter;
@@ -0,0 +1,90 @@
let mongodb = require('mongodb');
let Collection = mongodb.Collection;
export default class MongoCollection {
_mongoCollection:Collection;
constructor(mongoCollection:Collection) {
this._mongoCollection = mongoCollection;
}
// Does a find with "smart indexing".
// Currently this just means, if it needs a geoindex and there is
// none, then build the geoindex.
// This could be improved a lot but it's not clear if that's a good
// idea. Or even if this behavior is a good idea.
find(query, { skip, limit, sort } = {}) {
return this._rawFind(query, { skip, limit, sort })
.catch(error => {
// Check for "no geoindex" error
if (error.code != 17007 || !error.message.match(/unable to find index for .geoNear/)) {
throw error;
}
// Figure out what key needs an index
let key = error.message.match(/field=([A-Za-z_0-9]+) /)[1];
if (!key) {
throw error;
}
var index = {};
index[key] = '2d';
//TODO: condiser moving index creation logic into Schema.js
return this._mongoCollection.createIndex(index)
// Retry, but just once.
.then(() => this._rawFind(query, { skip, limit, sort }));
});
}
_rawFind(query, { skip, limit, sort } = {}) {
return this._mongoCollection
.find(query, { skip, limit, sort })
.toArray();
}
count(query, { skip, limit, sort } = {}) {
return this._mongoCollection.count(query, { skip, limit, sort });
}
// Atomically finds and updates an object based on query.
// The result is the promise with an object that was in the database !AFTER! changes.
// Postgres Note: Translates directly to `UPDATE * SET * ... RETURNING *`, which will return data after the change is done.
findOneAndUpdate(query, update) {
// arguments: query, sort, update, options(optional)
// Setting `new` option to true makes it return the after document, not the before one.
return this._mongoCollection.findAndModify(query, [], update, { new: true }).then(document => {
// Value is the object where mongo returns multiple fields.
return document.value;
});
}
insertOne(object) {
return this._mongoCollection.insertOne(object);
}
// Atomically updates data in the database for a single (first) object that matched the query
// If there is nothing that matches the query - does insert
// Postgres Note: `INSERT ... ON CONFLICT UPDATE` that is available since 9.5.
upsertOne(query, update) {
return this._mongoCollection.update(query, update, { upsert: true });
}
updateOne(query, update) {
return this._mongoCollection.updateOne(query, update);
}
updateMany(query, update) {
return this._mongoCollection.updateMany(query, update);
}
deleteOne(query) {
return this._mongoCollection.deleteOne(query);
}
deleteMany(query) {
return this._mongoCollection.deleteMany(query);
}
drop() {
return this._mongoCollection.drop();
}
}
@@ -0,0 +1,58 @@
import MongoCollection from './MongoCollection';
function _mongoSchemaQueryFromNameQuery(name: string, query) {
return _mongoSchemaObjectFromNameFields(name, query);
}
function _mongoSchemaObjectFromNameFields(name: string, fields) {
let object = { _id: name };
if (fields) {
Object.keys(fields).forEach(key => {
object[key] = fields[key];
});
}
return object;
}
export default class MongoSchemaCollection {
_collection: MongoCollection;
constructor(collection: MongoCollection) {
this._collection = collection;
}
getAllSchemas() {
return this._collection._rawFind({});
}
findSchema(name: string) {
return this._collection._rawFind(_mongoSchemaQueryFromNameQuery(name), { limit: 1 }).then(results => {
return results[0];
});
}
// Atomically find and delete an object based on query.
// The result is the promise with an object that was in the database before deleting.
// Postgres Note: Translates directly to `DELETE * FROM ... RETURNING *`, which will return data after delete is done.
findAndDeleteSchema(name: string) {
// arguments: query, sort
return this._collection._mongoCollection.findAndRemove(_mongoSchemaQueryFromNameQuery(name), []).then(document => {
// Value is the object where mongo returns multiple fields.
return document.value;
});
}
addSchema(name: string, fields) {
let mongoObject = _mongoSchemaObjectFromNameFields(name, fields);
return this._collection.insertOne(mongoObject);
}
updateSchema(name: string, update) {
return this._collection.updateOne(_mongoSchemaQueryFromNameQuery(name), update);
}
upsertSchema(name: string, query: string, update) {
return this._collection.upsertOne(_mongoSchemaQueryFromNameQuery(name, query), update);
}
}
@@ -0,0 +1,84 @@
import MongoCollection from './MongoCollection';
import MongoSchemaCollection from './MongoSchemaCollection';
import {parse as parseUrl, format as formatUrl} from '../../../vendor/mongodbUrl';
let mongodb = require('mongodb');
let MongoClient = mongodb.MongoClient;
const MongoSchemaCollectionName = '_SCHEMA';
export class MongoStorageAdapter {
// Private
_uri: string;
_options: Object;
// Public
connectionPromise;
database;
constructor(uri: string, options: Object) {
this._uri = uri;
this._options = options;
}
connect() {
if (this.connectionPromise) {
return this.connectionPromise;
}
// parsing and re-formatting causes the auth value (if there) to get URI
// encoded
const encodedUri = formatUrl(parseUrl(this._uri));
this.connectionPromise = MongoClient.connect(encodedUri, this._options).then(database => {
this.database = database;
});
return this.connectionPromise;
}
collection(name: string) {
return this.connect().then(() => {
return this.database.collection(name);
});
}
adaptiveCollection(name: string) {
return this.connect()
.then(() => this.database.collection(name))
.then(rawCollection => new MongoCollection(rawCollection));
}
schemaCollection(collectionPrefix: string) {
return this.connect()
.then(() => this.adaptiveCollection(collectionPrefix + MongoSchemaCollectionName))
.then(collection => new MongoSchemaCollection(collection));
}
collectionExists(name: string) {
return this.connect().then(() => {
return this.database.listCollections({ name: name }).toArray();
}).then(collections => {
return collections.length > 0;
});
}
dropCollection(name: string) {
return this.collection(name).then(collection => collection.drop());
}
// Used for testing only right now.
collectionsContaining(match: string) {
return this.connect().then(() => {
return this.database.collections();
}).then(collections => {
return collections.filter(collection => {
if (collection.namespace.match(/\.system\./)) {
return false;
}
return (collection.collectionName.indexOf(match) == 0);
});
});
}
}
export default MongoStorageAdapter;
module.exports = MongoStorageAdapter; // Required for tests
+48 -20
View File
@@ -2,15 +2,16 @@ var deepcopy = require('deepcopy');
var Parse = require('parse/node').Parse;
var RestQuery = require('./RestQuery');
var cache = require('./cache');
import cache from './cache';
// An Auth object tells you who is requesting something and whether
// the master key was used.
// userObject is a Parse.User and can be null if there's no user.
function Auth(config, isMaster, userObject) {
function Auth({ config, isMaster = false, user, installationId } = {}) {
this.config = config;
this.installationId = installationId;
this.isMaster = isMaster;
this.user = userObject;
this.user = user;
// Assuming a users roles won't change during a single request, we'll
// only load them once.
@@ -33,19 +34,19 @@ Auth.prototype.couldUpdateUserId = function(userId) {
// A helper to get a master-level Auth object
function master(config) {
return new Auth(config, true, null);
return new Auth({ config, isMaster: true });
}
// A helper to get a nobody-level Auth object
function nobody(config) {
return new Auth(config, false, null);
return new Auth({ config, isMaster: false });
}
// Returns a promise that resolves to an Auth object
var getAuthForSessionToken = function(config, sessionToken) {
var cachedUser = cache.getUser(sessionToken);
var getAuthForSessionToken = function({ config, sessionToken, installationId } = {}) {
var cachedUser = cache.users.get(sessionToken);
if (cachedUser) {
return Promise.resolve(new Auth(config, false, cachedUser));
return Promise.resolve(new Auth({ config, isMaster: false, installationId, user: cachedUser }));
}
var restOptions = {
limit: 1,
@@ -61,13 +62,20 @@ var getAuthForSessionToken = function(config, sessionToken) {
if (results.length !== 1 || !results[0]['user']) {
return nobody(config);
}
var now = new Date(),
expiresAt = new Date(results[0].expiresAt.iso);
if(expiresAt < now) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
'Session token is expired.');
}
var obj = results[0]['user'];
delete obj.password;
obj['className'] = '_User';
obj['sessionToken'] = sessionToken;
var userObject = Parse.Object.fromJSON(obj);
cache.setUser(sessionToken, userObject);
return new Auth(config, false, userObject);
let userObject = Parse.Object.fromJSON(obj);
cache.users.set(sessionToken, userObject);
return new Auth({ config, isMaster: false, installationId, user: userObject });
});
};
@@ -80,7 +88,7 @@ Auth.prototype.getUserRoles = function() {
return Promise.resolve(this.userRoles);
}
if (this.rolePromise) {
return rolePromise;
return this.rolePromise;
}
this.rolePromise = this._loadRoles();
return this.rolePromise;
@@ -109,8 +117,9 @@ Auth.prototype._loadRoles = function() {
var roleIDs = results.map(r => r.objectId);
var promises = [Promise.resolve(roleIDs)];
var queriedRoles = {};
for (var role of roleIDs) {
promises.push(this._getAllRoleNamesForId(role));
promises.push(this._getAllRoleNamesForId(role, queriedRoles));
}
return Promise.all(promises).then((results) => {
var allIDs = [];
@@ -138,18 +147,22 @@ Auth.prototype._loadRoles = function() {
};
// Given a role object id, get any other roles it is part of
// TODO: Make recursive to support role nesting beyond 1 level deep
Auth.prototype._getAllRoleNamesForId = function(roleID) {
Auth.prototype._getAllRoleNamesForId = function(roleID, queriedRoles = {}) {
// Don't need to requery this role as it is already being queried for.
if (queriedRoles[roleID] != null) {
return Promise.resolve([]);
}
queriedRoles[roleID] = true;
// As per documentation, a Role inherits AnotherRole
// if this Role is in the roles pointer of this AnotherRole
// Let's find all the roles where this role is in a roles relation
var rolePointer = {
__type: 'Pointer',
className: '_Role',
objectId: roleID
};
var restWhere = {
'$relatedTo': {
key: 'roles',
object: rolePointer
}
'roles': rolePointer
};
var query = new RestQuery(this.config, master(this.config), '_Role',
restWhere, {});
@@ -159,7 +172,22 @@ Auth.prototype._getAllRoleNamesForId = function(roleID) {
return Promise.resolve([]);
}
var roleIDs = results.map(r => r.objectId);
return Promise.resolve(roleIDs);
// we found a list of roles where the roleID
// is referenced in the roles relation,
// Get the roles where those found roles are also
// referenced the same way
var parentRolesPromises = roleIDs.map( (roleId) => {
return this._getAllRoleNamesForId(roleId, queriedRoles);
});
parentRolesPromises.push(Promise.resolve(roleIDs));
return Promise.all(parentRolesPromises);
}).then(function(results){
// Flatten
let roleIDs = results.reduce( (memo, result) => {
return memo.concat(result);
}, []);
return Promise.resolve([...new Set(roleIDs)]);
});
};
+121 -24
View File
@@ -1,33 +1,130 @@
// A Config object provides information about how a specific app is
// configured.
// mount is the URL for the root of the API; includes http, domain, etc.
function Config(applicationId, mount) {
var cache = require('./cache');
var DatabaseAdapter = require('./DatabaseAdapter');
var cacheInfo = cache.apps[applicationId];
this.valid = !!cacheInfo;
if (!this.valid) {
return;
import cache from './cache';
function removeTrailingSlash(str) {
if (!str) {
return str;
}
this.applicationId = applicationId;
this.collectionPrefix = cacheInfo.collectionPrefix || '';
this.masterKey = cacheInfo.masterKey;
this.clientKey = cacheInfo.clientKey;
this.javascriptKey = cacheInfo.javascriptKey;
this.dotNetKey = cacheInfo.dotNetKey;
this.restAPIKey = cacheInfo.restAPIKey;
this.fileKey = cacheInfo.fileKey;
this.facebookAppIds = cacheInfo.facebookAppIds;
this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers;
this.database = DatabaseAdapter.getDatabaseConnection(applicationId);
this.filesController = cacheInfo.filesController;
this.oauth = cacheInfo.oauth;
this.mount = mount;
if (str.endsWith("/")) {
str = str.substr(0, str.length-1);
}
return str;
}
export class Config {
constructor(applicationId: string, mount: string) {
let DatabaseAdapter = require('./DatabaseAdapter');
let cacheInfo = cache.apps.get(applicationId);
if (!cacheInfo) {
return;
}
this.applicationId = applicationId;
this.masterKey = cacheInfo.masterKey;
this.clientKey = cacheInfo.clientKey;
this.javascriptKey = cacheInfo.javascriptKey;
this.dotNetKey = cacheInfo.dotNetKey;
this.restAPIKey = cacheInfo.restAPIKey;
this.fileKey = cacheInfo.fileKey;
this.facebookAppIds = cacheInfo.facebookAppIds;
this.allowClientClassCreation = cacheInfo.allowClientClassCreation;
this.database = DatabaseAdapter.getDatabaseConnection(applicationId, cacheInfo.collectionPrefix);
this.serverURL = cacheInfo.serverURL;
this.publicServerURL = removeTrailingSlash(cacheInfo.publicServerURL);
this.verifyUserEmails = cacheInfo.verifyUserEmails;
this.appName = cacheInfo.appName;
this.hooksController = cacheInfo.hooksController;
this.filesController = cacheInfo.filesController;
this.pushController = cacheInfo.pushController;
this.loggerController = cacheInfo.loggerController;
this.userController = cacheInfo.userController;
this.authDataManager = cacheInfo.authDataManager;
this.customPages = cacheInfo.customPages || {};
this.mount = removeTrailingSlash(mount);
this.liveQueryController = cacheInfo.liveQueryController;
this.sessionLength = cacheInfo.sessionLength;
this.generateSessionExpiresAt = this.generateSessionExpiresAt.bind(this);
}
static validate(options) {
this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails,
appName: options.appName,
publicServerURL: options.publicServerURL})
if (options.publicServerURL) {
if (!options.publicServerURL.startsWith("http://") && !options.publicServerURL.startsWith("https://")) {
throw "publicServerURL should be a valid HTTPS URL starting with https://"
}
}
this.validateSessionLength(options.sessionLength);
}
static validateEmailConfiguration({verifyUserEmails, appName, publicServerURL}) {
if (verifyUserEmails) {
if (typeof appName !== 'string') {
throw 'An app name is required when using email verification.';
}
if (typeof publicServerURL !== 'string') {
throw 'A public server url is required when using email verification.';
}
}
}
get mount() {
var mount = this._mount;
if (this.publicServerURL) {
mount = this.publicServerURL;
}
return mount;
}
set mount(newValue) {
this._mount = newValue;
}
static validateSessionLength(sessionLength) {
if(isNaN(sessionLength)) {
throw 'Session length must be a valid number.';
}
else if(sessionLength <= 0) {
throw 'Session length must be a value greater than 0.'
}
}
generateSessionExpiresAt() {
var now = new Date();
return new Date(now.getTime() + (this.sessionLength*1000));
}
get invalidLinkURL() {
return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`;
}
get verifyEmailSuccessURL() {
return this.customPages.verifyEmailSuccess || `${this.publicServerURL}/apps/verify_email_success.html`;
}
get choosePasswordURL() {
return this.customPages.choosePassword || `${this.publicServerURL}/apps/choose_password`;
}
get requestResetPasswordURL() {
return `${this.publicServerURL}/apps/${this.applicationId}/request_password_reset`;
}
get passwordResetSuccessURL() {
return this.customPages.passwordResetSuccess || `${this.publicServerURL}/apps/password_reset_success.html`;
}
get verifyEmailURL() {
return `${this.publicServerURL}/apps/${this.applicationId}/verify_email`;
}
};
export default Config;
module.exports = Config;
+70
View File
@@ -0,0 +1,70 @@
/*
AdaptableController.js
AdaptableController is the base class for all controllers
that support adapter,
The super class takes care of creating the right instance for the adapter
based on the parameters passed
*/
// _adapter is private, use Symbol
var _adapter = Symbol();
import Config from '../Config';
export class AdaptableController {
constructor(adapter, appId, options) {
this.options = options;
this.appId = appId;
this.adapter = adapter;
}
set adapter(adapter) {
this.validateAdapter(adapter);
this[_adapter] = adapter;
}
get adapter() {
return this[_adapter];
}
get config() {
return new Config(this.appId);
}
expectedAdapterType() {
throw new Error("Subclasses should implement expectedAdapterType()");
}
validateAdapter(adapter) {
if (!adapter) {
throw new Error(this.constructor.name+" requires an adapter");
}
let Type = this.expectedAdapterType();
// Allow skipping for testing
if (!Type) {
return;
}
// Makes sure the prototype matches
let mismatches = Object.getOwnPropertyNames(Type.prototype).reduce( (obj, key) => {
const adapterType = typeof adapter[key];
const expectedType = typeof Type.prototype[key];
if (adapterType !== expectedType) {
obj[key] = {
expected: expectedType,
actual: adapterType
}
}
return obj;
}, {});
if (Object.keys(mismatches).length > 0) {
throw new Error("Adapter prototype don't match expected prototype", adapter, mismatches);
}
}
}
export default AdaptableController;
+659
View File
@@ -0,0 +1,659 @@
// A database adapter that works with data exported from the hosted
// Parse database.
import intersect from 'intersect';
var mongodb = require('mongodb');
var Parse = require('parse/node').Parse;
var Schema = require('./../Schema');
var transform = require('./../transform');
const deepcopy = require('deepcopy');
// options can contain:
// collectionPrefix: the string to put in front of every collection name.
function DatabaseController(adapter, { collectionPrefix } = {}) {
this.adapter = adapter;
this.collectionPrefix = collectionPrefix;
// We don't want a mutable this.schema, because then you could have
// one request that uses different schemas for different parts of
// it. Instead, use loadSchema to get a schema.
this.schemaPromise = null;
this.connect();
}
// Connects to the database. Returns a promise that resolves when the
// connection is successful.
DatabaseController.prototype.connect = function() {
return this.adapter.connect();
};
DatabaseController.prototype.adaptiveCollection = function(className) {
return this.adapter.adaptiveCollection(this.collectionPrefix + className);
};
DatabaseController.prototype.schemaCollection = function() {
return this.adapter.schemaCollection(this.collectionPrefix);
};
DatabaseController.prototype.collectionExists = function(className) {
return this.adapter.collectionExists(this.collectionPrefix + className);
};
DatabaseController.prototype.dropCollection = function(className) {
return this.adapter.dropCollection(this.collectionPrefix + className);
};
function returnsTrue() {
return true;
}
DatabaseController.prototype.validateClassName = function(className) {
if (!Schema.classNameIsValid(className)) {
const error = new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'invalid className: ' + className);
return Promise.reject(error);
}
return Promise.resolve();
};
// Returns a promise for a schema object.
// If we are provided a acceptor, then we run it on the schema.
// If the schema isn't accepted, we reload it at most once.
DatabaseController.prototype.loadSchema = function(acceptor = returnsTrue) {
if (!this.schemaPromise) {
this.schemaPromise = this.schemaCollection().then(collection => {
delete this.schemaPromise;
return Schema.load(collection);
});
return this.schemaPromise;
}
return this.schemaPromise.then((schema) => {
if (acceptor(schema)) {
return schema;
}
this.schemaPromise = this.schemaCollection().then(collection => {
delete this.schemaPromise;
return Schema.load(collection);
});
return this.schemaPromise;
});
};
// Returns a promise for the classname that is related to the given
// classname through the key.
// TODO: make this not in the DatabaseController interface
DatabaseController.prototype.redirectClassNameForKey = function(className, key) {
return this.loadSchema().then((schema) => {
var t = schema.getExpectedType(className, key);
var match = t ? t.match(/^relation<(.*)>$/) : false;
if (match) {
return match[1];
} else {
return className;
}
});
};
// Uses the schema to validate the object (REST API format).
// Returns a promise that resolves to the new schema.
// This does not update this.schema, because in a situation like a
// batch request, that could confuse other users of the schema.
DatabaseController.prototype.validateObject = function(className, object, query, options) {
let schema;
let isMaster = !('acl' in options);
var aclGroup = options.acl || [];
return this.loadSchema().then(s => {
schema = s;
if (isMaster) {
return Promise.resolve();
}
return this.canAddField(schema, className, object, aclGroup);
}).then(() => {
return schema.validateObject(className, object, query);
});
};
// Like transform.untransformObject but you need to provide a className.
// Filters out any data that shouldn't be on this REST-formatted object.
DatabaseController.prototype.untransformObject = function(
schema, isMaster, aclGroup, className, mongoObject) {
var object = transform.untransformObject(schema, className, mongoObject);
if (className !== '_User') {
return object;
}
if (isMaster || (aclGroup.indexOf(object.objectId) > -1)) {
return object;
}
delete object.authData;
delete object.sessionToken;
return object;
};
// Runs an update on the database.
// Returns a promise for an object with the new values for field
// modifications that don't know their results ahead of time, like
// 'increment'.
// Options:
// acl: a list of strings. If the object to be updated has an ACL,
// one of the provided strings must provide the caller with
// write permissions.
DatabaseController.prototype.update = function(className, query, update, options) {
const originalUpdate = update;
// Make a copy of the object, so we don't mutate the incoming data.
update = deepcopy(update);
var acceptor = function(schema) {
return schema.hasKeys(className, Object.keys(query));
};
var isMaster = !('acl' in options);
var aclGroup = options.acl || [];
var mongoUpdate, schema;
return this.loadSchema(acceptor)
.then(s => {
schema = s;
if (!isMaster) {
return schema.validatePermission(className, aclGroup, 'update');
}
return Promise.resolve();
})
.then(() => this.handleRelationUpdates(className, query.objectId, update))
.then(() => this.adaptiveCollection(className))
.then(collection => {
var mongoWhere = transform.transformWhere(schema, className, query);
if (options.acl) {
var writePerms = [
{_wperm: {'$exists': false}}
];
for (var entry of options.acl) {
writePerms.push({_wperm: {'$in': [entry]}});
}
mongoWhere = {'$and': [mongoWhere, {'$or': writePerms}]};
}
mongoUpdate = transform.transformUpdate(schema, className, update);
return collection.findOneAndUpdate(mongoWhere, mongoUpdate);
})
.then(result => {
if (!result) {
return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
'Object not found.'));
}
return sanitizeDatabaseResult(originalUpdate, result);
});
};
function sanitizeDatabaseResult(originalObject, result) {
let response = {};
if (!result) {
return Promise.resolve(response);
}
Object.keys(originalObject).forEach(key => {
let keyUpdate = originalObject[key];
// determine if that was an op
if (keyUpdate && typeof keyUpdate === 'object' && keyUpdate.__op
&& ['Add', 'AddUnique', 'Remove', 'Increment'].indexOf(keyUpdate.__op) > -1) {
// only valid ops that produce an actionable result
response[key] = result[key];
}
});
return Promise.resolve(response);
}
// Processes relation-updating operations from a REST-format update.
// Returns a promise that resolves successfully when these are
// processed.
// This mutates update.
DatabaseController.prototype.handleRelationUpdates = function(className,
objectId,
update) {
var pending = [];
var deleteMe = [];
objectId = update.objectId || objectId;
var process = (op, key) => {
if (!op) {
return;
}
if (op.__op == 'AddRelation') {
for (var object of op.objects) {
pending.push(this.addRelation(key, className,
objectId,
object.objectId));
}
deleteMe.push(key);
}
if (op.__op == 'RemoveRelation') {
for (var object of op.objects) {
pending.push(this.removeRelation(key, className,
objectId,
object.objectId));
}
deleteMe.push(key);
}
if (op.__op == 'Batch') {
for (var x of op.ops) {
process(x, key);
}
}
};
for (var key in update) {
process(update[key], key);
}
for (var key of deleteMe) {
delete update[key];
}
return Promise.all(pending);
};
// Adds a relation.
// Returns a promise that resolves successfully iff the add was successful.
DatabaseController.prototype.addRelation = function(key, fromClassName, fromId, toId) {
let doc = {
relatedId: toId,
owningId : fromId
};
let className = `_Join:${key}:${fromClassName}`;
return this.adaptiveCollection(className).then((coll) => {
return coll.upsertOne(doc, doc);
});
};
// Removes a relation.
// Returns a promise that resolves successfully iff the remove was
// successful.
DatabaseController.prototype.removeRelation = function(key, fromClassName, fromId, toId) {
var doc = {
relatedId: toId,
owningId: fromId
};
let className = `_Join:${key}:${fromClassName}`;
return this.adaptiveCollection(className).then(coll => {
return coll.deleteOne(doc);
});
};
// Removes objects matches this query from the database.
// Returns a promise that resolves successfully iff the object was
// deleted.
// Options:
// acl: a list of strings. If the object to be updated has an ACL,
// one of the provided strings must provide the caller with
// write permissions.
DatabaseController.prototype.destroy = function(className, query, options = {}) {
var isMaster = !('acl' in options);
var aclGroup = options.acl || [];
var schema;
return this.loadSchema()
.then(s => {
schema = s;
if (!isMaster) {
return schema.validatePermission(className, aclGroup, 'delete');
}
return Promise.resolve();
})
.then(() => this.adaptiveCollection(className))
.then(collection => {
let mongoWhere = transform.transformWhere(schema, className, query);
if (options.acl) {
var writePerms = [
{ _wperm: { '$exists': false } }
];
for (var entry of options.acl) {
writePerms.push({ _wperm: { '$in': [entry] } });
}
mongoWhere = { '$and': [mongoWhere, { '$or': writePerms }] };
}
return collection.deleteMany(mongoWhere);
})
.then(resp => {
//Check _Session to avoid changing password failed without any session.
// TODO: @nlutsenko Stop relying on `result.n`
if (resp.result.n === 0 && className !== "_Session") {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
}
});
};
// Inserts an object into the database.
// Returns a promise that resolves successfully iff the object saved.
DatabaseController.prototype.create = function(className, object, options) {
// Make a copy of the object, so we don't mutate the incoming data.
let originalObject = object;
object = deepcopy(object);
var schema;
var isMaster = !('acl' in options);
var aclGroup = options.acl || [];
return this.validateClassName(className)
.then(() => this.loadSchema())
.then(s => {
schema = s;
if (!isMaster) {
return schema.validatePermission(className, aclGroup, 'create');
}
return Promise.resolve();
})
.then(() => this.handleRelationUpdates(className, null, object))
.then(() => this.adaptiveCollection(className))
.then(coll => {
var mongoObject = transform.transformCreate(schema, className, object);
return coll.insertOne(mongoObject);
})
.then(result => {
return sanitizeDatabaseResult(originalObject, result.ops[0]);
});
};
DatabaseController.prototype.canAddField = function(schema, className, object, aclGroup) {
let classSchema = schema.data[className];
if (!classSchema) {
return Promise.resolve();
}
let fields = Object.keys(object);
let schemaFields = Object.keys(classSchema);
let newKeys = fields.filter((field) => {
return schemaFields.indexOf(field) < 0;
})
if (newKeys.length > 0) {
return schema.validatePermission(className, aclGroup, 'addField');
}
return Promise.resolve();
}
// Runs a mongo query on the database.
// This should only be used for testing - use 'find' for normal code
// to avoid Mongo-format dependencies.
// Returns a promise that resolves to a list of items.
DatabaseController.prototype.mongoFind = function(className, query, options = {}) {
return this.adaptiveCollection(className)
.then(collection => collection.find(query, options));
};
// Deletes everything in the database matching the current collectionPrefix
// Won't delete collections in the system namespace
// Returns a promise.
DatabaseController.prototype.deleteEverything = function() {
this.schemaPromise = null;
return this.adapter.collectionsContaining(this.collectionPrefix).then(collections => {
let promises = collections.map(collection => {
return collection.drop();
});
return Promise.all(promises);
});
};
// Finds the keys in a query. Returns a Set. REST format only
function keysForQuery(query) {
var sublist = query['$and'] || query['$or'];
if (sublist) {
let answer = sublist.reduce((memo, subquery) => {
return memo.concat(keysForQuery(subquery));
}, []);
return new Set(answer);
}
return new Set(Object.keys(query));
}
// Returns a promise for a list of related ids given an owning id.
// className here is the owning className.
DatabaseController.prototype.relatedIds = function(className, key, owningId) {
return this.adaptiveCollection(joinTableName(className, key))
.then(coll => coll.find({owningId : owningId}))
.then(results => results.map(r => r.relatedId));
};
// Returns a promise for a list of owning ids given some related ids.
// className here is the owning className.
DatabaseController.prototype.owningIds = function(className, key, relatedIds) {
return this.adaptiveCollection(joinTableName(className, key))
.then(coll => coll.find({ relatedId: { '$in': relatedIds } }))
.then(results => results.map(r => r.owningId));
};
// Modifies query so that it no longer has $in on relation fields, or
// equal-to-pointer constraints on relation fields.
// Returns a promise that resolves when query is mutated
DatabaseController.prototype.reduceInRelation = function(className, query, schema) {
// Search for an in-relation or equal-to-relation
// Make it sequential for now, not sure of paralleization side effects
if (query['$or']) {
let ors = query['$or'];
return Promise.all(ors.map((aQuery, index) => {
return this.reduceInRelation(className, aQuery, schema).then((aQuery) => {
query['$or'][index] = aQuery;
})
}));
}
let promises = Object.keys(query).map((key) => {
if (query[key] && (query[key]['$in'] || query[key]['$ne'] || query[key]['$nin'] || query[key].__type == 'Pointer')) {
let t = schema.getExpectedType(className, key);
let match = t ? t.match(/^relation<(.*)>$/) : false;
if (!match) {
return Promise.resolve(query);
}
let relatedClassName = match[1];
// Build the list of queries
let queries = Object.keys(query[key]).map((constraintKey) => {
let relatedIds;
let isNegation = false;
if (constraintKey === 'objectId') {
relatedIds = [query[key].objectId];
} else if (constraintKey == '$in') {
relatedIds = query[key]['$in'].map(r => r.objectId);
} else if (constraintKey == '$nin') {
isNegation = true;
relatedIds = query[key]['$nin'].map(r => r.objectId);
} else if (constraintKey == '$ne') {
isNegation = true;
relatedIds = [query[key]['$ne'].objectId];
} else {
return;
}
return {
isNegation,
relatedIds
}
});
// remove the current queryKey as we don,t need it anymore
delete query[key];
// execute each query independnently to build the list of
// $in / $nin
let promises = queries.map((q) => {
if (!q) {
return Promise.resolve();
}
return this.owningIds(className, key, q.relatedIds).then((ids) => {
if (q.isNegation) {
this.addNotInObjectIdsIds(ids, query);
} else {
this.addInObjectIdsIds(ids, query);
}
return Promise.resolve();
});
});
return Promise.all(promises).then(() => {
return Promise.resolve();
})
}
return Promise.resolve();
})
return Promise.all(promises).then(() => {
return Promise.resolve(query);
})
};
// Modifies query so that it no longer has $relatedTo
// Returns a promise that resolves when query is mutated
DatabaseController.prototype.reduceRelationKeys = function(className, query) {
if (query['$or']) {
return Promise.all(query['$or'].map((aQuery) => {
return this.reduceRelationKeys(className, aQuery);
}));
}
var relatedTo = query['$relatedTo'];
if (relatedTo) {
return this.relatedIds(
relatedTo.object.className,
relatedTo.key,
relatedTo.object.objectId).then((ids) => {
delete query['$relatedTo'];
this.addInObjectIdsIds(ids, query);
return this.reduceRelationKeys(className, query);
});
}
};
DatabaseController.prototype.addInObjectIdsIds = function(ids = null, query) {
let idsFromString = typeof query.objectId === 'string' ? [query.objectId] : null;
let idsFromEq = query.objectId && query.objectId['$eq'] ? [query.objectId['$eq']] : null;
let idsFromIn = query.objectId && query.objectId['$in'] ? query.objectId['$in'] : null;
let allIds = [idsFromString, idsFromEq, idsFromIn, ids].filter(list => list !== null);
let totalLength = allIds.reduce((memo, list) => memo + list.length, 0);
let idsIntersection = [];
if (totalLength > 125) {
idsIntersection = intersect.big(allIds);
} else {
idsIntersection = intersect(allIds);
}
// Need to make sure we don't clobber existing $lt or other constraints on objectId.
// Clobbering $eq, $in and shorthand $eq (query.objectId === 'string') constraints
// is expected though.
if (!('objectId' in query) || typeof query.objectId === 'string') {
query.objectId = {};
}
query.objectId['$in'] = idsIntersection;
return query;
}
DatabaseController.prototype.addNotInObjectIdsIds = function(ids = null, query) {
let idsFromNin = query.objectId && query.objectId['$nin'] ? query.objectId['$nin'] : null;
let allIds = [idsFromNin, ids].filter(list => list !== null);
let totalLength = allIds.reduce((memo, list) => memo + list.length, 0);
let idsIntersection = [];
if (totalLength > 125) {
idsIntersection = intersect.big(allIds);
} else {
idsIntersection = intersect(allIds);
}
// Need to make sure we don't clobber existing $lt or other constraints on objectId.
// Clobbering $eq, $in and shorthand $eq (query.objectId === 'string') constraints
// is expected though.
if (!('objectId' in query) || typeof query.objectId === 'string') {
query.objectId = {};
}
query.objectId['$nin'] = idsIntersection;
return query;
}
// Runs a query on the database.
// Returns a promise that resolves to a list of items.
// Options:
// skip number of results to skip.
// limit limit to this number of results.
// sort an object where keys are the fields to sort by.
// the value is +1 for ascending, -1 for descending.
// count run a count instead of returning results.
// acl restrict this operation with an ACL for the provided array
// of user objectIds and roles. acl: null means no user.
// when this field is not present, don't do anything regarding ACLs.
// TODO: make userIds not needed here. The db adapter shouldn't know
// anything about users, ideally. Then, improve the format of the ACL
// arg to work like the others.
DatabaseController.prototype.find = function(className, query, options = {}) {
let mongoOptions = {};
if (options.skip) {
mongoOptions.skip = options.skip;
}
if (options.limit) {
mongoOptions.limit = options.limit;
}
let isMaster = !('acl' in options);
let aclGroup = options.acl || [];
let acceptor = schema => schema.hasKeys(className, keysForQuery(query))
let schema = null;
return this.loadSchema(acceptor).then(s => {
schema = s;
if (options.sort) {
mongoOptions.sort = {};
for (let key in options.sort) {
let mongoKey = transform.transformKey(schema, className, key);
mongoOptions.sort[mongoKey] = options.sort[key];
}
}
if (!isMaster) {
let op = typeof query.objectId == 'string' && Object.keys(query).length === 1 ?
'get' :
'find';
return schema.validatePermission(className, aclGroup, op);
}
return Promise.resolve();
})
.then(() => this.reduceRelationKeys(className, query))
.then(() => this.reduceInRelation(className, query, schema))
.then(() => this.adaptiveCollection(className))
.then(collection => {
let mongoWhere = transform.transformWhere(schema, className, query);
if (!isMaster) {
let orParts = [
{"_rperm" : { "$exists": false }},
{"_rperm" : { "$in" : ["*"]}}
];
for (let acl of aclGroup) {
orParts.push({"_rperm" : { "$in" : [acl]}});
}
mongoWhere = {'$and': [mongoWhere, {'$or': orParts}]};
}
if (options.count) {
delete mongoOptions.limit;
return collection.count(mongoWhere, mongoOptions);
} else {
return collection.find(mongoWhere, mongoOptions)
.then((mongoResults) => {
return mongoResults.map((r) => {
return this.untransformObject(
schema, isMaster, aclGroup, className, r);
});
});
}
});
};
function joinTableName(className, key) {
return `_Join:${key}:${className}`;
}
module.exports = DatabaseController;
+33 -101
View File
@@ -1,88 +1,42 @@
// FilesController.js
import express from 'express';
import mime from 'mime';
import { Parse } from 'parse/node';
import BodyParser from 'body-parser';
import * as Middlewares from '../middlewares';
import Config from '../Config';
import { randomHexString } from '../cryptoUtils';
import AdaptableController from './AdaptableController';
import { FilesAdapter } from '../Adapters/Files/FilesAdapter';
import path from 'path';
import mime from 'mime';
export class FilesController {
constructor(filesAdapter) {
this._filesAdapter = filesAdapter;
export class FilesController extends AdaptableController {
getFileData(config, filename) {
return this.adapter.getFileData(filename);
}
getHandler() {
return (req, res) => {
let config = new Config(req.params.appId);
let filename = req.params.filename;
this._filesAdapter.getFileData(config, filename).then((data) => {
res.status(200);
var contentType = mime.lookup(filename);
res.set('Content-type', contentType);
res.end(data);
}).catch((error) => {
res.status(404);
res.set('Content-type', 'text/plain');
res.end('File not found.');
createFile(config, filename, data, contentType) {
let extname = path.extname(filename);
const hasExtension = extname.length > 0;
if (!hasExtension && contentType && mime.extension(contentType)) {
filename = filename + '.' + mime.extension(contentType);
} else if (hasExtension && !contentType) {
contentType = mime.lookup(filename);
}
filename = randomHexString(32) + '_' + filename;
var location = this.adapter.getFileLocation(config, filename);
return this.adapter.createFile(filename, data, contentType).then(() => {
return Promise.resolve({
url: location,
name: filename
});
};
});
}
createHandler() {
return (req, res, next) => {
if (!req.body || !req.body.length) {
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR,
'Invalid file upload.'));
return;
}
if (req.params.filename.length > 128) {
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
'Filename too long.'));
return;
}
if (!req.params.filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) {
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
'Filename contains invalid characters.'));
return;
}
// If a content-type is included, we'll add an extension so we can
// return the same content-type.
let extension = '';
let hasExtension = req.params.filename.indexOf('.') > 0;
let contentType = req.get('Content-type');
if (!hasExtension && contentType && mime.extension(contentType)) {
extension = '.' + mime.extension(contentType);
}
let filename = randomHexString(32) + '_' + req.params.filename + extension;
this._filesAdapter.createFile(req.config, filename, req.body).then(() => {
res.status(201);
var location = this._filesAdapter.getFileLocation(req.config, filename);
res.set('Location', location);
res.json({ url: location, name: filename });
}).catch((error) => {
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR,
'Could not store file.'));
});
};
}
deleteHandler() {
return (req, res, next) => {
this._filesAdapter.deleteFile(req.config, req.params.filename).then(() => {
res.status(200);
// TODO: return useful JSON here?
res.end();
}).catch((error) => {
next(new Parse.Error(Parse.Error.FILE_DELETE_ERROR,
'Could not delete file.'));
});
};
deleteFile(config, filename) {
return this.adapter.deleteFile(filename);
}
/**
@@ -108,36 +62,14 @@ export class FilesController {
if (filename.indexOf('tfss-') === 0) {
fileObject['url'] = 'http://files.parsetfss.com/' + config.fileKey + '/' + encodeURIComponent(filename);
} else {
fileObject['url'] = this._filesAdapter.getFileLocation(config, filename);
fileObject['url'] = this.adapter.getFileLocation(config, filename);
}
}
}
}
getExpressRouter() {
let router = express.Router();
router.get('/files/:appId/:filename', this.getHandler());
router.post('/files', function(req, res, next) {
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
'Filename not provided.'));
});
router.post('/files/:filename',
Middlewares.allowCrossDomain,
BodyParser.raw({type: '*/*', limit: '20mb'}),
Middlewares.handleParseHeaders,
this.createHandler()
);
router.delete('/files/:filename',
Middlewares.allowCrossDomain,
Middlewares.handleParseHeaders,
Middlewares.enforceMasterKeyAccess,
this.deleteHandler()
);
return router;
expectedAdapterType() {
return FilesAdapter;
}
}
+216
View File
@@ -0,0 +1,216 @@
/** @flow weak */
import * as DatabaseAdapter from "../DatabaseAdapter";
import * as triggers from "../triggers";
import * as Parse from "parse/node";
import * as request from "request";
const DefaultHooksCollectionName = "_Hooks";
export class HooksController {
_applicationId:string;
_collectionPrefix:string;
_collection;
constructor(applicationId:string, collectionPrefix:string = '') {
this._applicationId = applicationId;
this._collectionPrefix = collectionPrefix;
}
load() {
return this._getHooks().then(hooks => {
hooks = hooks || [];
hooks.forEach((hook) => {
this.addHookToTriggers(hook);
});
});
}
getCollection() {
if (this._collection) {
return Promise.resolve(this._collection)
}
let database = DatabaseAdapter.getDatabaseConnection(this._applicationId, this._collectionPrefix);
return database.adaptiveCollection(DefaultHooksCollectionName).then(collection => {
this._collection = collection;
return collection;
});
}
getFunction(functionName) {
return this._getHooks({ functionName: functionName }, 1).then(results => results[0]);
}
getFunctions() {
return this._getHooks({ functionName: { $exists: true } });
}
getTrigger(className, triggerName) {
return this._getHooks({ className: className, triggerName: triggerName }, 1).then(results => results[0]);
}
getTriggers() {
return this._getHooks({ className: { $exists: true }, triggerName: { $exists: true } });
}
deleteFunction(functionName) {
triggers.removeFunction(functionName, this._applicationId);
return this._removeHooks({ functionName: functionName });
}
deleteTrigger(className, triggerName) {
triggers.removeTrigger(triggerName, className, this._applicationId);
return this._removeHooks({ className: className, triggerName: triggerName });
}
_getHooks(query, limit) {
let options = limit ? { limit: limit } : undefined;
return this.getCollection().then(collection => collection.find(query, options));
}
_removeHooks(query) {
return this.getCollection().then(collection => {
return collection.deleteMany(query);
}).then(() => {
return {};
});
}
saveHook(hook) {
var query;
if (hook.functionName && hook.url) {
query = { functionName: hook.functionName }
} else if (hook.triggerName && hook.className && hook.url) {
query = { className: hook.className, triggerName: hook.triggerName }
} else {
throw new Parse.Error(143, "invalid hook declaration");
}
return this.getCollection()
.then(collection => collection.upsertOne(query, hook))
.then(() => {
return hook;
});
}
addHookToTriggers(hook) {
var wrappedFunction = wrapToHTTPRequest(hook);
wrappedFunction.url = hook.url;
if (hook.className) {
triggers.addTrigger(hook.triggerName, hook.className, wrappedFunction, this._applicationId)
} else {
triggers.addFunction(hook.functionName, wrappedFunction, null, this._applicationId);
}
}
addHook(hook) {
this.addHookToTriggers(hook);
return this.saveHook(hook);
}
createOrUpdateHook(aHook) {
var hook;
if (aHook && aHook.functionName && aHook.url) {
hook = {};
hook.functionName = aHook.functionName;
hook.url = aHook.url;
} else if (aHook && aHook.className && aHook.url && aHook.triggerName && triggers.Types[aHook.triggerName]) {
hook = {};
hook.className = aHook.className;
hook.url = aHook.url;
hook.triggerName = aHook.triggerName;
} else {
throw new Parse.Error(143, "invalid hook declaration");
}
return this.addHook(hook);
};
createHook(aHook) {
if (aHook.functionName) {
return this.getFunction(aHook.functionName).then((result) => {
if (result) {
throw new Parse.Error(143, `function name: ${aHook.functionName} already exits`);
} else {
return this.createOrUpdateHook(aHook);
}
});
} else if (aHook.className && aHook.triggerName) {
return this.getTrigger(aHook.className, aHook.triggerName).then((result) => {
if (result) {
throw new Parse.Error(143, `class ${aHook.className} already has trigger ${aHook.triggerName}`);
}
return this.createOrUpdateHook(aHook);
});
}
throw new Parse.Error(143, "invalid hook declaration");
};
updateHook(aHook) {
if (aHook.functionName) {
return this.getFunction(aHook.functionName).then((result) => {
if (result) {
return this.createOrUpdateHook(aHook);
}
throw new Parse.Error(143, `no function named: ${aHook.functionName} is defined`);
});
} else if (aHook.className && aHook.triggerName) {
return this.getTrigger(aHook.className, aHook.triggerName).then((result) => {
if (result) {
return this.createOrUpdateHook(aHook);
}
throw new Parse.Error(143, `class ${aHook.className} does not exist`);
});
}
throw new Parse.Error(143, "invalid hook declaration");
};
}
function wrapToHTTPRequest(hook) {
return (req, res) => {
let jsonBody = {};
for (var i in req) {
jsonBody[i] = req[i];
}
if (req.object) {
jsonBody.object = req.object.toJSON();
jsonBody.object.className = req.object.className;
}
if (req.original) {
jsonBody.original = req.original.toJSON();
jsonBody.original.className = req.original.className;
}
let jsonRequest = {
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(jsonBody)
};
request.post(hook.url, jsonRequest, function (err, httpResponse, body) {
var result;
if (body) {
if (typeof body == "string") {
try {
body = JSON.parse(body);
} catch (e) {
err = { error: "Malformed response", code: -1 };
}
}
if (!err) {
result = body.success;
err = body.error;
}
}
if (err) {
return res.error(err);
} else {
return res.success(result);
}
});
}
}
export default HooksController;
+51
View File
@@ -0,0 +1,51 @@
import { ParseCloudCodePublisher } from '../LiveQuery/ParseCloudCodePublisher';
export class LiveQueryController {
classNames: any;
liveQueryPublisher: any;
constructor(config: any) {
let classNames;
// If config is empty, we just assume no classs needs to be registered as LiveQuery
if (!config || !config.classNames) {
this.classNames = new Set();
} else if (config.classNames instanceof Array) {
this.classNames = new Set(config.classNames);
} else {
throw 'liveQuery.classes should be an array of string'
}
this.liveQueryPublisher = new ParseCloudCodePublisher(config);
}
onAfterSave(className: string, currentObject: any, originalObject: any) {
if (!this.hasLiveQuery(className)) {
return;
}
let req = this._makePublisherRequest(currentObject, originalObject);
this.liveQueryPublisher.onCloudCodeAfterSave(req);
}
onAfterDelete(className: string, currentObject: any, originalObject: any) {
if (!this.hasLiveQuery(className)) {
return;
}
let req = this._makePublisherRequest(currentObject, originalObject);
this.liveQueryPublisher.onCloudCodeAfterDelete(req);
}
hasLiveQuery(className: string): boolean {
return this.classNames.has(className);
}
_makePublisherRequest(currentObject: any, originalObject: any): any {
let req = {
object: currentObject
};
if (currentObject) {
req.original = originalObject;
}
return req;
}
}
export default LiveQueryController;

Some files were not shown because too many files have changed in this diff Show More