Compare commits

..
778 Commits
Author SHA1 Message Date
Steven Shipton 593a7a6264 Release 2.3.7 2017-03-10 18:05:13 +00:00
steven-supersolid 4addf9dfa9 Update version to 2.3.7 2017-03-10 17:49:55 +00:00
Steven Shipton 2547fd08ae Update CHANGELOG.md
style nits
2017-03-10 17:15:34 +00:00
Steven Shipton 581f919d1a Changelog 2.3.7 2017-03-10 17:12:56 +00:00
Steven Shipton 0b1a61cd85 Merge pull request #3614 from ParsePlatform/greenkeeper-request-2.81.0
Update request to version 2.81.0 🚀
2017-03-10 16:37:00 +00:00
greenkeeperio-bot 943b84b13f chore(package): update request to version 2.81.0
https://greenkeeper.io/
2017-03-09 08:09:13 -08:00
Steven Shipton 70f60267c5 upgrade body-parser and express (#3605) 2017-03-07 08:53:53 -08:00
Greenkeeper f0f04d6351 chore(package): update body-parser to version 1.17.0 (#3589)
https://greenkeeper.io/
2017-03-05 09:11:53 -08:00
Arthur Cinader 1d8edd73c3 default for userSensitiveFields should be an array not a string. (#3599) 2017-03-05 09:06:45 -08:00
Greenkeeper 8d802b9461 chore(package): update request to version 2.80.0 (#3596)
https://greenkeeper.io/
2017-03-04 15:57:37 -08:00
Vitaly Tomilov 271608bc82 Update PostgresStorageAdapter.js (#3578)
* Update PostgresStorageAdapter.js

proper database API, via transaction.

* Update PostgresStorageAdapter.js

fixing bracket.

* Update PostgresStorageAdapter.js

adding the same rejection approach as before, the functionality remains identical.

* Update PostgresStorageAdapter.js

* Update PostgresStorageAdapter.js

lint fixing

* Update PostgresStorageAdapter.js

fixing misspelling.
2017-03-04 15:56:53 -08:00
Greenkeeper 5c5ef0313b chore(package): update ws to version 2.2.0 (#3580)
https://greenkeeper.io/
2017-03-04 17:50:54 -06:00
Xy Ziemba 29fec01a42 Resend Verification Email Endpoint (#3543)
* Endpoint to Handle Verification Email Request

* Adds tests for verificationEmailRequest endpoint

* Better error responses for `/verificationEmailRequest`
2017-03-04 13:30:52 -08:00
Greenkeeper 92b699920e chore(package): update express to version 4.15.0 (#3590)
https://greenkeeper.io/
2017-03-04 13:27:59 -08:00
Natan Rolnik 9bfa0c60c4 Adds index on _Role name property (#3586)
* Adds index on _Role name property

In order to avoid having different _Role objects with the same name, adding an index on the name property of _Role is necessary.

Fixes #3579

* Uses throw instead of Promise.reject when enforcing unique indexes

* Fixes wrong sorting of results in schemas tests
2017-03-04 12:42:19 -08:00
Greenkeeper 0181fb51b3 chore(package): update winston-daily-rotate-file to version 1.4.5 (#3594)
https://greenkeeper.io/
2017-03-03 11:57:25 -08:00
Ryan Foster e6006e800e adding TTL option for redis cache adapter (#3397)
* adding TTL option for redis cache adapter

* adding test for RedisCacheAdapter

* Fixing adapater var name

* changing timeout

* updating default time

* Fix the redis cache spec to construct the cache with the anticipated ttl

make timeout values really really small so our test run fast :).
2017-02-27 12:29:15 -08:00
Florent Vilmart a45d1848db version 2.3.6 (#3574)
* version 2.3.6

* Update CHANGELOG.md
2017-02-27 19:10:06 +02:00
Greenkeeper 6381e15e6d chore(package): update pg-promise to version 5.6.2 (#3573)
https://greenkeeper.io/
2017-02-27 11:45:43 -05:00
Jack Wearden 73195627c0 Alleviate SERVER-13732 on all top level filters (#3564)
In a prior commit, improvements were made to the addition of `_rperm`
in the case of `$or` queries, to avoid MongoDB bug SERVER-13732.

As the vast majority of $or queries previously hit this bug due to the
presence of `_rperm` on most Parse queries), the present solution
avoids the bug and improves query performance in most cases.

However, it's still possible for clients to supply their own queries
which hit that bug, such as those with `_created_at` or `_updated_at`
filters, or their own properties from their data model.

This commit makes the logic currently present for `_rperm` available
to all top level filters that exist alongside an $or query, meaning
SERVER-13732 should be avoided in all cases where keys at the top and
inner levels do not have name clashes.

- https://github.com/ParsePlatform/parse-server/pull/3476
- https://jira.mongodb.org/browse/SERVER-13732
2017-02-26 13:37:33 -05:00
Greenkeeper 053bbefda9 chore(package): update pg-promise to version 5.6.1 (#3572)
https://greenkeeper.io/
2017-02-26 13:34:19 -05:00
Paulo Vítor S Reis 032d5e55ca Logger MaskSentive just for strings and array string items (#3571) 2017-02-26 13:34:03 -05:00
shaukatnamal fe5947ce4c chore(package): update eslint to version 3.16.1 (#3563)
https://greenkeeper.io/
2017-02-24 16:18:57 -08:00
Bhaskar Reddy Yasa 0e900cbefd allow flow through to passwordPolicy in case of empty ('') password (#3560) 2017-02-24 07:21:50 -05:00
Florent Vilmart 41358d2226 Adds ability to pass a middleware to CLI for instrumentation (#3554)
* Adds ability to pass a middleware to CLI for instrumentation

* Adds readme
2017-02-22 23:42:21 +02:00
Greenkeeper 73260897cd chore(package): update pg-promise to version 5.5.8 (#3551)
https://greenkeeper.io/
2017-02-21 14:28:12 -08:00
Florent Vilmart d3e6c0dea3 Adds test for #3544 (#3545) 2017-02-21 15:06:45 -05:00
Florent Vilmart b4e27e1160 Adds testing safeguard on lib (#3537) 2017-02-20 22:45:45 +02:00
Greenkeeper 79b0c790f0 chore(package): update uws to version 0.13.0 (#3542)
https://greenkeeper.io/
2017-02-20 12:26:44 -08:00
Greenkeeper 64f608ec9f chore(package): update ws to version 2.1.0 (#3526)
https://greenkeeper.io/
2017-02-20 12:25:39 -08:00
Tyler Brock 67260b5fe0 Factor out checking for triggers/liveQuery in rest.js (#3539) 2017-02-20 12:25:25 -08:00
Florent Vilmart 411daf4d41 Release 2.3.5 2017-02-19 11:34:42 -05:00
Natan Rolnik 7a3c9cd33a Changelog 2.3.4 (#3533) 2017-02-19 08:44:33 -05:00
Florent Vilmart 193e5a4278 Make sure we don't treat dot notation keys as topLevel atoms (#3531)
Fixing GeoPoints and Files in _GlobalConfig
2017-02-19 12:07:54 +02:00
Jeremy Louie 6ae0675010 Use flushdb instead of flushall in RedisCacheAdapter (#3523) 2017-02-18 19:36:43 -05:00
David Starke a520ea0243 Fix LiveQuery unsafe user (#3525)
* LiveQuery should not use unsafe user setting

* server should issue queries with the master key
2017-02-18 19:36:20 -05:00
Greenkeeper 58bdeeee2d chore(package): update pg-promise to version 5.5.7 (#3528)
https://greenkeeper.io/
2017-02-18 19:24:32 -05:00
Vitaly Tomilov fc7f9f76f5 removing inline sql. (#3529)
* removing inline sql.

* Update index.js

trying to fix indentation.

* Update index.js

commenting out console output for errors.

* Rename json-object-set-key.sql to json-object-set-keys.sql

* Update index.js

* Update add-unique.sql

correcting sql.

* Update add.sql

correcting sql.

* Update contains-all.sql

correcting sql.

* Update contains.sql

correcting sql.

* Update remove.sql

correcting sql.

* Update json-object-set-keys.sql

fixing sql reserved words.
2017-02-18 14:26:25 -05:00
Greenkeeper ef8d534679 chore(package): update babel-core to version 6.23.1 (#3512)
https://greenkeeper.io/
2017-02-17 10:18:12 -08:00
Greenkeeper 1c9852e53f chore(package): update babel-core to version 6.23.0 (#3509)
https://greenkeeper.io/
2017-02-17 10:16:43 -08:00
Greenkeeper 3f0de0c2ed chore(package): update babel-register to version 6.23.0 (#3510)
https://greenkeeper.io/
2017-02-17 10:14:27 -08:00
Greenkeeper eccdeccb59 chore(package): update babel-cli to version 6.23.0 (#3511)
https://greenkeeper.io/
2017-02-17 10:14:10 -08:00
Greenkeeper f16f8423db chore(package): update mongodb to version 2.2.24 (#3513)
https://greenkeeper.io/
2017-02-17 10:13:55 -08:00
Greenkeeper bd60a5c1d1 chore(package): update body-parser to version 1.16.1 (#3499)
https://greenkeeper.io/
2017-02-11 15:03:05 -08:00
Robin Naundorf 40b9f953ca Fix small display errors (#3500)
* Add missing '$' to declare variable
* Move \n to correct position
2017-02-11 12:24:09 -05:00
Héctor Ramos 15eafd4dcc Update ISSUE_TEMPLATE.md 2017-02-10 11:56:58 -08:00
Héctor Ramos 11cde12245 Merge pull request #3497 from acinader/allow-empty-client-key
Allow empty client key
2017-02-09 16:16:49 -08:00
Arthur Cinader 5861996cb0 explicitly check if auth keys are undefined
Simply checking if they are truthy causes a false negative
if the value is ''.
2017-02-09 15:20:10 -08:00
Arthur Cinader ca70ad0141 Add a unit test to fail when clientKey='' 2017-02-09 14:50:28 -08:00
Natan Rolnik 5a22df13a5 Changelog 2.3.3 (#3493) 2017-02-09 17:13:15 +02:00
Greenkeeper 5b7144176c chore(package): update ws to version 2.0.3 (#3488)
https://greenkeeper.io/
2017-02-08 10:43:00 -08:00
Arthur Cinader 4979503f63 Two tests that fail often. Give em a little more time. (#3453) 2017-02-08 10:06:42 -08:00
Greenkeeper e32a37c2ec chore(package): update bcryptjs to version 2.4.3 (#3485)
https://greenkeeper.io/
2017-02-08 09:09:29 -08:00
Florent Vilmart f7f17cdb23 Update package.json (#3480)
* Update package.json

* Update .travis.yml
2017-02-06 10:41:33 -07:00
nodechefMatt ecf422b2d9 Mongo object to Parse object date serialization - avoid re-serialization of iso of type Date (#3389)
* Mongo object to Parse object date serialization - avoid nested ios

* Mongo object to Parse object date serialization

* Remove file from previous commit
2017-02-04 08:32:35 -08:00
Greenkeeper 49dc8df1de chore(package): update babel-cli to version 6.22.2 (#3411)
https://greenkeeper.io/
2017-02-03 21:40:32 -08:00
Greenkeeper c3c5f06efd chore(package): update babel-register to version 6.22.0 (#3409)
https://greenkeeper.io/
2017-02-03 21:40:21 -08:00
Greenkeeper 2a454fec35 chore(package): update babel-preset-es2015 to version 6.22.0 (#3408)
https://greenkeeper.io/
2017-02-03 21:40:07 -08:00
Greenkeeper 01b4c4e0cb chore(package): update babel-plugin-transform-flow-strip-types to version 6.22.0 (#3406)
https://greenkeeper.io/
2017-02-03 21:39:18 -08:00
Greenkeeper 3714d68aaf chore(package): update babel-preset-stage-0 to version 6.22.0 (#3407)
https://greenkeeper.io/
2017-02-03 21:38:49 -08:00
Greenkeeper 77daa3ca18 chore(package): update ws to version 2.0.2 (#3475)
https://greenkeeper.io/
2017-02-03 21:37:24 -08:00
Greenkeeper 0b574f04a9 chore(package): update babel-core to version 6.22.1 (#3412)
https://greenkeeper.io/
2017-02-03 21:37:05 -08:00
Greenkeeper a89a30182e chore(package): update mongodb to version 2.2.22 (#3426)
https://greenkeeper.io/
2017-02-03 21:36:53 -08:00
Jack Wearden 96a9588a6e MongoDB $or Queries avoid SERVER-13732 bug (#3476)
MongoDB has an unfixed bug in all supported versions 2.6-3.4 which
results in suboptimal index usage for `$or` queries when the query has
implicit `$and`s at the query root.

When adding `_rperm` to `$or` queries, Parse accidentally creates
queries which hit this bug.

The fix in this commit applies the suggested workaround of putting the
`_rperm` property inside all `$or` subdocuments, moving it from the top
level and leaving `$or` as the only top-level operator.

MongoDB Bug Link: https://jira.mongodb.org/browse/SERVER-13732
2017-02-03 21:34:19 -08:00
Greenkeeper 66cdfaed4b chore(package): update ws to version 2.0.1 (#3465)
https://greenkeeper.io/
2017-02-02 14:05:05 -08:00
Greenkeeper 9262147a6f chore(package): update pg-promise to version 5.5.6 (#3461)
https://greenkeeper.io/
2017-02-02 12:00:07 -08:00
Greenkeeper 8b4e9ca637 chore(package): update multer to version 1.3.0 (#3434)
https://greenkeeper.io/
2017-02-02 11:52:20 -08:00
Greenkeeper 2160d10990 chore(package): update redis to version 2.6.5 (#3377)
https://greenkeeper.io/
2017-02-02 09:10:21 -08:00
Arthur Cinader e7f2995143 Adjust a test for change to logging. (#3452) 2017-01-30 13:28:52 -05:00
Arthur Cinader d5940b17b6 Add logging on failure to create file (#3424) 2017-01-30 12:36:30 -05:00
Arthur Cinader f864141663 Log Parse Errors so they are intelligible. (#3431)
The problem this pr is trying to solve:

When an error occurs on the server, a message should
be returned to the client, and a message should be logged.

Currently, on the server, the log is just [object, object]

This pr will stop calling the default express error handler
which causes two problems: 1. it writes to console instead of log file
2. the output is completely useless! :)

Instead, we'll log the error ourselves using the ParseServer's logger.

fixes: #661
2017-01-30 12:32:54 -05:00
Elliot Blackburn 711db9ccd2 Remove duplicate word from the README log section (#3450)
In the "Logging" section it originally read: "Parse Server, will, by default, will log:", I've removed the second "will" to make "Parse Server will, by default, log:".
2017-01-30 14:39:18 +02:00
Greenkeeper 6a6829ce80 chore(package): update express to version 4.14.1 (#3445)
https://greenkeeper.io/
2017-01-29 15:08:47 -05:00
Ricardo Matsui d67be492f5 Verify pushes are sent to the expected number of installations (#3418) 2017-01-28 11:10:21 -08:00
Greenkeeper ea45f69af6 chore(package): update winston-daily-rotate-file to version 1.4.4 (#3442)
https://greenkeeper.io/
2017-01-28 09:18:21 -08:00
Greenkeeper 43c3d8e2a2 chore(package): update winston to version 2.3.1 (#3414)
https://greenkeeper.io/
2017-01-23 10:29:40 -07:00
Greenkeeper d8a1a614e8 chore(package): update body-parser to version 1.16.0 (#3391)
https://greenkeeper.io/
2017-01-19 13:42:17 -08:00
Greenkeeper 553db0ada5 chore(package): update pg-promise to version 5.5.2 (#3379)
https://greenkeeper.io/
2017-01-19 11:01:03 -08:00
Greenkeeper 388a59d816 chore(package): update pg-promise to version 5.5.1 (#3362)
https://greenkeeper.io/
2017-01-19 11:00:34 -08:00
Greenkeeper de4c1ee418 Update mongodb to version 2.2.21 🚀 (#3368)
* chore(package): update mongodb to version 2.2.20

https://greenkeeper.io/

* Fix race when fetching schema frequently

* Bumps mongo to 2.2.21
2017-01-16 13:26:14 -08:00
Florent Vilmart cb15c0b616 Disallow failures for PG (#3372) 2017-01-16 13:24:45 -08:00
Greenkeeper 8dd864a033 chore(package): update parse-server-push-adapter to version 1.2.0 (#3386)
https://greenkeeper.io/
2017-01-16 11:27:19 -05:00
Florent Vilmart c26db2248f use uWS as optional dependency for ws server (#3231)
* use uWS as optional dependency for ws server

* Update ParseWebSocketServer.js
2017-01-14 07:43:31 -08:00
Greenkeeper b2a3479dbf chore(package): update winston-daily-rotate-file to version 1.4.3 (#3369)
https://greenkeeper.io/
2017-01-14 07:40:57 -08:00
Florent Vilmart deedf7b370 Push scalability (#3080)
* Update status through increment
* adds support for incrementing nested keys
* fix issue when having spaces in keys for ordering
* Refactors PushController to use worker
* Adds tests for custom push queue config
* Makes PushController adapter independant
* Better logging of _PushStatus in VERBOSE
2017-01-13 19:34:04 -05:00
Greenkeeper 5f849ca662 chore(package): update redis to version 2.6.4 (#3364)
https://greenkeeper.io/
2017-01-12 21:45:14 -05:00
Greenkeeper 0279b5e5f3 chore(package): update jasmine to version 2.5.3 (#3358)
https://greenkeeper.io/
2017-01-11 18:54:23 -08:00
Arthur Cinader 4cb6e7d209 Add lint rule space-infix-ops (#3237)
Disallows: 1+1.  Must be 1 + 1.
2017-01-11 12:31:40 -08:00
Greenkeeper 56bb505df2 chore(package): update jasmine-spec-reporter to version 3.1.0 (#3344)
https://greenkeeper.io/
2017-01-11 12:31:15 -08:00
Arthur Cinader ceb5a1b5f0 chore(package): update jasmine-spec-reporter to version 3.0.0 (#3333)
https://greenkeeper.io/

Fix parse-server to work with jasmine-spec-reporter-3.0.0
2017-01-11 11:53:24 -08:00
Natan Rolnik 0958b15297 Changelog 2.3.2 (#3343)
* Updates package.json: version to 2.3.2

* Updates Changelog for 2.3.2
2017-01-09 15:46:34 +02:00
Lenart Rudel 5d9dbea07b Add parseFrameURL for masking user-facing pages (#3267)
* Add parseFrameURL for masking user-facing pages.

Allow users to specify a different address which is used to mask parse
requests for verifying email and resetting password. This is how Parse.com
used to allow customers to gain control over page content, styling etc.

On the destination page javascript is used to check the link in the request
and embed the parse server page using IFRAME.

* Fix code indentation

* Rename method for building link and pass config to it.

* Add customPages options to README.md.

* Add tests for parseFrameURL email link building, and parseFrameURL option.

* Add parseFrameURL for masking user-facing pages.

Allow users to specify a different address which is used to mask parse
requests for verifying email and resetting password. This is how Parse.com
used to allow customers to gain control over page content, styling etc.

On the destination page javascript is used to check the link in the request
and embed the parse server page using IFRAME.

* Fix code indentation

* Rename method for building link and pass config to it.

* Add customPages options to README.md.

* Don't Object.assign to defaultConfiguration global
2017-01-08 10:56:57 -08:00
Bhaskar Reddy Yasa f331f6644c Add support for regex string for password policy validatorPattern setting (#3331) 2017-01-08 07:12:44 -08:00
David Starke df029b82eb LiveQuery should match subobjects with dot notation (#3322)
* LiveQuery should match subobjects with dot notation

* one additional test case
2017-01-06 17:06:25 -08:00
Arthur Cinader edba550cf6 winston-daily-roate-file-1.4.2 makes a change where timestamp is not on by default anymore. (#3335)
see: https://github.com/winstonjs/winston-daily-rotate-file/commit/aa28f522718778fe8210ced1090353ffc29c6f31

pretty incredible that they would release this as a patch release, but my fix is solid and defensive

chore(package): update winston-daily-rotate-file to version 1.4.2

https://greenkeeper.io/
2017-01-06 16:32:03 -08:00
Greenkeeper c7775eb0b9 chore(package): update cross-env to version 3.1.4 (#3308)
https://greenkeeper.io/
2017-01-06 14:50:09 -08:00
Greenkeeper 0b44820a7d chore(package): update winston-daily-rotate-file to version 1.4.1 (#3317)
https://greenkeeper.io/
2017-01-06 13:20:12 -08:00
Greenkeeper b7322064d8 chore(package): update pg-promise to version 5.5.0 (#3224)
https://greenkeeper.io/
2017-01-04 09:41:21 -05:00
jeacott1 ac3f5cd686 this function was extremely slow. (#3264)
this fix reduces the time to process 165k installations
from 282738.599ms (almost 5 minutes of total server lockup!)
to just 16.283ms!
2017-01-04 09:27:47 -05:00
Greenkeeper 9fd34fd25e chore(package): update bcryptjs to version 2.4.0 (#3225)
https://greenkeeper.io/
2017-01-03 16:42:59 -06:00
Greenkeeper 7ff3a3813d chore(package): update babel-core to version 6.21.0 (#3249)
https://greenkeeper.io/
2017-01-02 15:04:18 -06:00
Greenkeeper 754f5d0505 chore(package): update babel-plugin-transform-flow-strip-types to version 6.21.0 (#3250)
https://greenkeeper.io/
2017-01-02 14:40:30 -06:00
Greenkeeper 0e8c3c2457 chore(package): update multer to version 1.2.1 (#3276)
https://greenkeeper.io/
2017-01-02 14:09:24 -06:00
Greenkeeper 423a50133a chore(package): update lodash to version 4.17.4 (#3303)
https://greenkeeper.io/
2017-01-02 13:25:37 -06:00
Greenkeeper a3d416de4a chore(package): update bcrypt to version 1.0.2 (#3304)
https://greenkeeper.io/
2017-01-01 12:01:59 -06:00
Greenkeeper a2dfcc8da8 chore(package): update babel-core to version 6.20.0 (#3216)
https://greenkeeper.io/
2017-01-01 08:44:38 -06:00
Arthur Cinader f4734a65c0 fix trivial typo (#3238) 2016-12-14 14:28:23 -08:00
Florent Vilmart aa071e034e Changelog 2.3.1 (#3213)
* Update CHANGELOG.md

* Update package.json
2016-12-08 14:30:59 -05:00
Florent Vilmart 4f46eceacf Fixes issue affecting the auth providers (#3211)
* Reproduces error for issue 3208

* Make sure we don't override a default adapter with the options when validation function are not provided
2016-12-08 08:54:06 -08:00
Florent Vilmart 81ebbb8954 Update README.md 2016-12-08 10:16:31 -05:00
Greenkeeper e82395e268 chore(package): update bcrypt to version 1.0.1 (#3207)
https://greenkeeper.io/
2016-12-08 09:55:35 -05:00
Florent Vilmart 0159360a5d Fixes CI dist (#3206) 2016-12-07 19:57:13 -08:00
Florent Vilmart aab10bea79 Changelog 2.3.0 (#3205)
* 2.3.0 changelog

* bumps package

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md
2016-12-07 20:48:54 -05:00
Florent Vilmart 752f0e9143 Fixes #3195 (#3201)
* Adds error reproduction

* Fix transform in order to accept nested array results in $in/$nin

The error originated by the nesting of 2 array in $in [["..."]], using _.flatMap with those will guarantee at the lower level
that the query is properly resolved

* nits

* _.flatMap the $in/$nin values
2016-12-07 20:03:40 -05:00
Florent Vilmart 025e7a3218 Adds depreciation notes for Parse.Cloud.useMasterKey(), (#3204)
Adds depreciation notes for Parse.Cloud.useMasterKey()
2016-12-07 18:35:22 -05:00
Arthur Cinader ca286b7108 Enable prefer-const lint rule (#3202) 2016-12-07 18:17:05 -05:00
Florent Vilmart a6c988176e Adds tests that shows issue #3194 is ok (#3200) 2016-12-07 14:18:52 -08:00
corban baxter e069b20ef9 adding simple test for adding and removing users on a role. (#3198) 2016-12-07 16:15:01 -05:00
Arthur Cinader ae30ec647b let's see if i can force latest to rebuild... (#3196) 2016-12-07 08:57:01 -08:00
Greenkeeper fc53fa9c32 chore(package): update parse-server-s3-adapter to version 1.0.6 (#3192)
https://greenkeeper.io/
2016-12-06 16:56:26 -08:00
Florent Vilmart c1dcaf1271 Auth Adapters refactoring (#3177)
* Moves all authentication providers to Adapter/Auth

* refactors specs

* Deprecates oauth option in favor of auth option

- Deprecates facebookAppIds option (in favor of auth.facebook.appIds)
- Adds warnings about the deprecated options

* nits
2016-12-06 14:09:43 -08:00
Florent Vilmart a9067260fc adds ability to pass custom installationId to create sessions in cloud code (#3156) 2016-12-06 16:46:09 -05:00
Florent Vilmart 2e569ca7dd Fixes SchemaController data for Volatile Classes (#3171)
* Reproduction for the issue

* Ensures Volatile classes and other schema share the same structure
2016-12-06 16:42:54 -05:00
Arthur Cinader 8111c3338e turn on max empty line rule with default of max: 2. (#3176)
Run with fix.
2016-12-06 13:35:23 -08:00
Florent Vilmart fb36dfa26f Fix #3185 (#3186)
* Adds tests that reproduce the issue

* Use values from keys to force include when needed
2016-12-06 16:28:55 -05:00
Andrew Lane 998b271994 HyperDev changed its name to Gomix, so reflect that in the README (#3191) 2016-12-06 10:44:37 -08:00
Florent Vilmart a7821f6e57 Removes NodeChef (#3188)
NodeChef explicitly runs a private fork of parse-server, we should not support this.

Explicitely, Scheduled Push and Cloud Code browser are most requested features, that they didn't provide back to the community.
2016-12-06 10:39:21 -05:00
Greenkeeper ddb6f7bc57 chore(package): update bcrypt to version 1.0.0 (#3178)
https://greenkeeper.io/
2016-12-05 19:08:46 -08:00
Kulshekhar Kabra 8124bf4600 Add a CLI option to specify the host (#3180) 2016-12-05 08:50:46 -05:00
Kulshekhar Kabra df74a1f8ae Use trusty in travis (#3170)
* Use trusty in travis

* Install postgresql-9.5-postgis-2.3 in travis
2016-12-03 18:43:30 -05:00
Florent Vilmart e0704b440c Adds class level permission requiring authenticated user (#893)
* Adds class level permission requiring authenticated user

* Updates to latest schema permissions syntax

* fix flaky test

* Exclude PG

* Rebased and nitted

* lints
2016-12-02 19:47:33 -05:00
Arthur Cinader 01b05b060f Strip personally identifiable information from user table for unauthorized users.
- add a config option to explicitly enumerate pii fields beyond email
- in query controller, strip pii of user table results before sending out the door.
2016-12-02 19:39:09 -05:00
Arthur Cinader a270632570 More lint tweaking (#3164)
1. Add no space in paren rule
2. fix spec/eslintrc.json so it allow for inheriting from root rc.

Because the spce rc specified reccomended, it "turned off" all of the
rule tweaks in the root.  This fixes that.
2016-12-02 19:11:54 -05:00
Florent Vilmart b9afccd338 Makes sudo: false (#3166)
* Makes sudo: false

* ignore error when configuring postgis

* Use precise instead of trusty
2016-12-02 17:53:32 -05:00
Arthur Cinader 37daafc7b0 Add strip whitespace and trailing eol to spec too. (#3159) 2016-12-01 21:45:49 -05:00
Arthur Cinader 8359284329 Fix some random minor typos. (#3157) 2016-12-01 20:34:39 -05:00
Greenkeeper 2b0c39caaa chore(package): update pg-promise to version 5.4.5 (#3140)
https://greenkeeper.io/
2016-12-01 17:59:10 -05:00
Arthur Cinader 11f7712651 Add no trailing whitespace and force eol at end of file. (#3154) 2016-12-01 13:24:46 -05:00
Arthur Cinader 7292fa7f11 Allow select (keys) to be altered in triggers (#3146)
Inspect the keys when a query is returned from a trigger and respect the new
value.
2016-12-01 10:04:47 -05:00
Arthur Cinader 4d17bed374 lint before test. (#3147)
Also turn on lint cacheing to make this change painless ;)
add lint cache to gitignore...
2016-11-30 17:58:59 -05:00
Steven Shipton 143bd11638 Change arg check from truthy to defined (#3143)
Show log on master when using cluster
2016-11-30 08:48:49 -05:00
Bhaskar Reddy Yasa bd1689190f Adds password history support to passwordPolicy (#3102)
* password history support in passwordPolicy

* Refactor RestWrite.transformUser

* fix eslint issues
2016-11-29 09:01:52 -08:00
Greenkeeper 08bac9f790 chore(package): update pg-promise to version 5.4.4 (#3121)
https://greenkeeper.io/
2016-11-29 12:01:49 -05:00
Greenkeeper f2df0303b2 chore(package): update winston-daily-rotate-file to version 1.4.0 (#3132)
https://greenkeeper.io/
2016-11-29 10:53:24 -05:00
Steven Shipton fbb2b086af ensure batch routing paths are posix (#3138) 2016-11-29 09:52:17 -05:00
Florent Vilmart 127629d4f7 Changelog 2.2.25 (#3134) 2016-11-28 16:42:41 -05:00
Florent Vilmart 6676c6a4b9 Let multiple installations be updates if no critical values are set (#3040)
* Let multiple installations be updates if no critical values are set

* nits
2016-11-28 13:11:54 -05:00
Florent Vilmart b6fb3d8d51 Update CONTRIBUTING.md (#3109)
Note on linting
2016-11-28 10:38:55 -05:00
Antonio Davi Macedo Coelho de Castro 6264f99ad5 Concat appId to PubSub channel (#3043) 2016-11-28 09:15:21 -05:00
Greenkeeper ee9099d6ad chore(package): update lru-cache to version 4.0.2 (#3127)
https://greenkeeper.io/
2016-11-28 09:14:17 -05:00
jonas-db c83a787510 Use the id of the account instead of the application id (#3108)
* Use the id of the account instead of the application id

Fix for issue: https://github.com/ParsePlatform/parse-server/issues/3106

* Update twitter.js

* Update twitter.js
2016-11-25 14:05:41 -05:00
Yuki Takeichi 85567310d4 Ignores createdAt when update (#3111) 2016-11-25 09:20:06 -05:00
David Poetzsch-Heffter 8f1c1f419b fixing equals on array columns in live query (#3089) 2016-11-24 20:42:53 -05:00
Florent Vilmart d800ff85e2 Enhanced support for batch endpoints (#3042)
* Allow to have different endpoint on publicserverURL and serverURL when batching

* nits
2016-11-24 20:14:12 -05:00
Florent Vilmart 8c2c76dd26 Adds liniting into the workflow (#3082)
* initial linting of src

* fix indent to 2 spaces

* Removes unnecessary rules

* ignore spec folder for now

* Spec linting

* Fix spec indent

* nits

* nits

* no no-empty rule
2016-11-24 15:47:41 -05:00
Florent Vilmart 6e2fba4ae4 Moves test runner logic to helper (#3041) 2016-11-24 15:09:11 -05:00
haifeizhang e4a7fbbf98 add login with qq, wechat and weibo (#3069)
* add login with qq, wechat and weibo

* modify the code style, etc
2016-11-22 08:30:31 -05:00
Bhaskar Reddy Yasa edb7b70ced Adds password expiry support to password policy (#3068)
* Adding support for password expiry policy

* Renamed daysBeforeExpiry -> maxPasswordAge
2016-11-21 07:46:38 -08:00
Florent Vilmart 11c6170ed1 Adds support for postgres when saving relation #3074 (#3084) 2016-11-21 09:22:16 -05:00
Andrew Imm d137e27eb9 Properly expose TestUtils through top-level package (#3086) 2016-11-21 08:53:16 -05:00
Antonio Davi Macedo Coelho de Castro a28257b2b7 Enabling web hooks (#3044) 2016-11-20 18:39:47 -05:00
Diwakar Cherukumilli f22cea5ca9 Merge pull request #3083 from ParsePlatform/greenkeeper-pg-promise-5.4.3
Update pg-promise to version 5.4.3 🚀
2016-11-20 15:32:10 -08:00
greenkeeperio-bot 0bc53753b0 chore(package): update pg-promise to version 5.4.3
https://greenkeeper.io/
2016-11-20 10:35:51 -08:00
Glen Tregoning 1eff210a51 MongoStorageAdapter.findOneAndUpdate returns Parse Object (#3053) (#3064) 2016-11-20 09:02:49 -05:00
Yuki Takeichi 4ea455b20a support Relation object saving (#3074)
* support Parse.Relation object save

* prevent Relation object from being saved in storage
2016-11-20 09:00:33 -05:00
Greenkeeper e9e6a4764e chore(package): update pg-promise to version 5.4.2 (#3081)
https://greenkeeper.io/
2016-11-20 08:58:43 -05:00
Diwakar Cherukumilli 4785891e99 Merge pull request #3071 from ParsePlatform/greenkeeper-request-2.79.0
Update request to version 2.79.0 🚀
2016-11-18 14:41:54 -08:00
greenkeeperio-bot 8e1e89b794 chore(package): update request to version 2.79.0
https://greenkeeper.io/
2016-11-18 09:33:12 -08:00
Bhaskar Reddy Yasa cf6ce5b9a3 Adding support for optional Password Policy (#3032)
* adds resetTokenValidityDuration setting

* adds a validator to validate password that can be used to enforce strong
passwords

* adds unit tests for passwordPolicy.validator

* adds unit tests to to fail reset password function if password is not in a valid format

* updates README.md for passwordPolicy

* prevents duplicate check for password validator in updateUserPassword

* adds optional setting to disallow username in password

* updates test cases to use fdescribe instead of describe

* updates test cases to use request-promise instead of request

* adds ability to use a RegExp or Callback function or both for a passwordPolicy.validator

* expect username parameter in redirect to password_reset_success

* adds support for _perishable_token_expires_at in postgres
2016-11-17 08:37:51 -08:00
Diwakar Cherukumilli 6be9ee5491 Merge pull request #3061 from ParsePlatform/greenkeeper-lodash-4.17.2
Update lodash to version 4.17.2 🚀
2016-11-16 12:36:14 -06:00
greenkeeperio-bot fe5f0a8980 chore(package): update lodash to version 4.17.2
https://greenkeeper.io/
2016-11-15 23:40:01 -08:00
Diwakar Cherukumilli 303851d86d Merge pull request #3054 from ParsePlatform/greenkeeper-lodash-4.17.1
Update lodash to version 4.17.1 🚀
2016-11-15 13:57:36 -06:00
greenkeeperio-bot 0cfae4d603 chore(package): update lodash to version 4.17.1
https://greenkeeper.io/
2016-11-14 23:21:44 -08:00
Florent Vilmart 7e0e48457b Basic test for CLI definitions (#3050)
* Add rudimentary test for cli definitions.
Use consistent import and quote style.

* nit: style

* Fix bug.
Add similar tests for live query definitions
2016-11-14 11:38:28 -05:00
steven-supersolid 7b5f89e037 Fix bug.
Add similar tests for live query definitions
2016-11-14 16:23:31 +00:00
steven-supersolid 7bc83c372d nit: style 2016-11-14 15:14:48 +00:00
steven-supersolid 28c1824394 Add rudimentary test for cli definitions.
Use consistent import and quote style.
2016-11-14 14:59:00 +00:00
jb 19271fa1ef Adding support for AfterFind (#2968) 2016-11-12 12:35:34 -05:00
Florent Vilmart b80d8a0d3d Fix #3038 (#3039) 2016-11-11 09:40:41 -08:00
Tyler Brock e9dfb68a37 Add support for maxTimeMS mongoOption (#3018) 2016-11-11 08:03:35 -08:00
Florent Vilmart fbbc23772b deploy to dockerHub on tags (#3011) 2016-11-11 10:27:01 -05:00
Steven Shipton 5082609937 Allow numberOrBooleanParser to parse string correctly. Improve error message in numberParser. Add env variable PARSE_SERVER_CLUSTER for cluster. Add tests (#3034) 2016-11-11 09:40:45 -05:00
Halim Qarroum 9abf17730f Injected the username when password has changed. (#3033)
* Injected the username when password has changed.

When the resetPassword controller has successfully updated the user's password, it now redirects the user to the password changed page with the username as a query parameters.

* Update unit test for password_reset_success redirect
2016-11-10 12:40:17 -05:00
Steven Shipton bb9ce773bb Don't create default folder when CLI parameter is 'null' (#3006) 2016-11-04 09:11:57 -04:00
Kirill Kravinsky 1029ddf563 Fix error in docs (#3007) 2016-11-04 09:11:13 -04:00
Kirill Kravinsky 71fb6cef5e Dockerizing Parse Server (#3005)
* Add Dockerfile

* Add Docker image docs

* Remove default PARSE_SERVER_CLOUD_CODE_MAIN setting + update docs

* Update docs

* Update documentation
2016-11-03 17:38:05 -04:00
Tyler Brock 2fb4f89324 Fix whitespace in getDatabaseAdapter 2016-11-03 10:53:37 -07:00
Greenkeeper 1ae3613608 chore(package): update request to version 2.78.0 (#3002)
https://greenkeeper.io/
2016-11-03 10:01:51 -04:00
Steven Shipton 5f335a9d30 Fix parameter name (#3001) 2016-11-03 10:01:31 -04:00
Florent Vilmart fb6150feb1 2.2.25-beta.1 (#2995) 2016-11-02 20:43:45 -04:00
Kirill Kravinsky 908c48098a Add health-checking endpoint on '/health' that always returns 200 (#2992)
* Add health-checker endpoint on '/health' that always returns 200OK

* Refactor health handler, add tests

* Refactor health test
2016-11-02 20:33:59 -04:00
Steven Shipton b347bff641 Add option to re-use schema cache between requests (#2979)
* Add option to reuse database controller between requests. Clear schema cache when deleting everything

* Add test

* Rename setting to persistSchemaCache to more accurately reflect effect

* Repurpose option to determine whether to randomize cache prefix. Restore Config.js controller creation. Add tests

* Fix bug with missing parameter passed to to SchemaCache

* Renaming and formatting

* Fix property name typo

* Rename option to avoid double negative and still be falsey by default. Style fix
2016-11-02 19:05:23 -04:00
Tyler Brock 801308d9b7 RedisCacheAdapter: add some additional checks and defaults (#2991) 2016-11-02 15:13:55 -07:00
Greenkeeper af700d7a4a chore(package): update redis to version 2.6.3 (#2975)
https://greenkeeper.io/
2016-11-02 11:41:02 -07:00
Greenkeeper 9c2cbd254e chore(package): update winston to version 2.3.0 (#2986)
https://greenkeeper.io/
2016-11-02 10:56:46 -07:00
Tyler Brock 33ebedfd6a Expire values in RedisCacheAdapter (#2988) 2016-11-02 08:39:49 -04:00
Tyler Brock f3c656829a Use spec reporter instead of dots (#2989) 2016-11-02 08:39:09 -04:00
Greenkeeper 86fdd173a5 chore(package): update babel-core to version 6.18.2 (#2983)
https://greenkeeper.io/
2016-11-01 22:39:52 -07:00
Kulshekhar Kabra 583887c43c Improve update of jsonb fields. Add PG 9.5 to travis. (#2984)
* Improve update of jsonb fields. Add PG 9.5 to travis.

* Replace manual escaping with pg-promise's built in
2016-11-01 20:55:53 -04:00
Kulshekhar Kabra 8f0ae7081d Enable a few tests to test against Postgres (#2971)
* Enable RestCreate related tests for Postgres

* Enable tests in ParseRole for postgres

* Enable one ParseObject test for postgres

* Improve the way a jsonb field is updated.

* Use Postgres 9.5 in Travis

* Install Postgres 9.5 in Travis

* Use sudo to install and setup Postgres 9.5 in Travis

* Test if Postgres 9.5 can be used with trusty

* Re-exclude a test for postgres

* Revert travis to its original form
2016-11-01 07:30:22 -04:00
Florent Vilmart 5c4806ba6d Changelog 2.2.24 (#2976) 2016-10-31 19:30:20 -04:00
Kulshekhar Kabra 78646a3e91 Enable API related tests for Postgres (#2970) 2016-10-31 12:11:21 -04:00
Kulshekhar Kabra edf6ab6875 Enable query related tests for Postgres (#2969) 2016-10-31 12:10:53 -04:00
Greenkeeper 142b3beab6 chore(package): update lodash to version 4.16.5 (#2972)
https://greenkeeper.io/
2016-10-31 08:19:04 -04:00
Kulshekhar Kabra b6c19a53ee Fix endsWith query test to use the correct query (#2965) 2016-10-31 08:12:58 -04:00
Florent Vilmart 48865c765f Adds ability to update a subscription (#2935)
* Adds ability to update a subscription

* Adds unsubscribe to the RequestSchema, makes sure to not fire unsubscribe to the client when updating

* Fix failing tests

* More extensive tests

* fix annotation
2016-10-31 08:12:11 -04:00
Michael Helvey 94178df4d2 Add functionName to invalid function error (#2963)
Currently in the logs if an invalid function is called the error
message just says "invalid function" which is decidedly unhelpful
when looking through server logs.  Now it will say
"Invalid function: "functionName""
2016-10-30 11:58:34 -04:00
Kulshekhar Kabra 4a2f7ff2db Close client connections on SIGINT/SIGTERM (#2964)
* Close all alive connections on SIGINT/SIGTERM

* Add a comment referencing the node issue.
2016-10-30 11:58:07 -04:00
Kulshekhar Kabra 22c790f23c Wrap postgres class creation in a transaction (#2958) 2016-10-28 20:23:37 -04:00
Florent Vilmart 0e78c28146 Fix PG fails (#2957)
* Better support for arrays of objects

* No transaction in class creation
2016-10-28 16:43:52 -04:00
Florent Vilmart 23b77f7261 Live query pubsub adapter (#2902)
* Moves LiveQuery pub/sub to adapter folder

* Adds ability to provide custom adapter for LiveQuery pubsub

* Adds test for function based adapter

* Pass all options to createSubscriber

* nits
2016-10-28 12:06:35 -04:00
Kulshekhar Kabra f23c0a57ee Fix the error returned when class already exists (#2955)
* Fix the error returned when class already exists

* Wrap the class creation in a transaction
2016-10-28 11:46:19 -04:00
Kulshekhar Kabra 4a5ed1095c Allow the URI for the test DB to be set via env (#2954) 2016-10-28 09:11:07 -04:00
Steven Shipton e788d49af0 Don't require all keys to be configured to enable key checks (#2816) (#2941)
* Add tests. Fail request if any of the 4 optional keys does not match

* Only require one key to be supplied in the request, except when no keys are configured

* Use const over let, var
2016-10-26 16:44:39 -04:00
Florent Vilmart 278027a955 Google auth: try to validate on both sub or user_id (#2922) 2016-10-26 11:44:56 -04:00
Greenkeeper f4fe19a55e chore(package): update request to version 2.76.0 (#2932)
https://greenkeeper.io/
2016-10-25 08:56:27 -04:00
Greenkeeper 403fb3e062 chore(package): update babel-register to version 6.18.0 (#2928)
https://greenkeeper.io/
2016-10-24 22:51:28 -04:00
Greenkeeper 3df10dca88 chore(package): update babel-core to version 6.18.0 (#2925)
https://greenkeeper.io/
2016-10-24 21:35:21 -04:00
Greenkeeper 68dbe005b5 chore(package): update babel-plugin-transform-flow-strip-types to version 6.18.0 (#2926)
https://greenkeeper.io/
2016-10-24 21:35:12 -04:00
Greenkeeper 8c4be63a4c chore(package): update babel-cli to version 6.18.0 (#2924)
https://greenkeeper.io/
2016-10-24 19:26:23 -04:00
Greenkeeper e2a5228127 chore(package): update pg-promise to version 5.3.5 (#2915)
https://greenkeeper.io/
2016-10-24 17:24:46 -04:00
Steven Shipton 151bea25ee Try to retrieve schema from all schemas cache if not found in individual cache (#2912)
* Try to get schema from main schema if not found in single schema

* Add newline

* Add missing return

* Add missing done to tests
2016-10-23 11:59:39 -04:00
Aaron Blondeau af55cd1efb Add role based ACL checks to LiveQuery (#2893)
* Add acl role check to _matchesACL, start adding tests.

* Add tests for ACL role checks in LiveQueryServer.

* Switch to arrow functions, add immutabalized code from @acinader, swap for loop style.
2016-10-21 23:53:31 -04:00
Florent Vilmart 0faaec3224 Release notes for 2.2.23 (#2895) 2016-10-19 18:01:12 -07:00
Marian Ignev 021fe4d336 Add SashiDo Migration guides to README (#2896) 2016-10-19 20:56:43 -04:00
Florent Vilmart 5dc24334a5 Adds ability to run livequery server on different port (appengine) (#2892)
* Adds ability to run livequery server on different port (appengine)
2016-10-19 17:11:25 -07:00
Florent Vilmart 86f35014f4 Fixes #2780 (#2810)
* Adds regression test for #2780

* Fixes #2780

- Make sure we compare installationId from the data and not the auth when rejecting update
2016-10-19 19:54:19 -04:00
Yuki Takeichi 758975cf03 Revert " Saved filename fixed so that clients display it correctly #2415 (#2705)" (#2833)
This reverts commit 22c1a87d80.
2016-10-19 16:55:20 -04:00
Jeremy Louie a6a6f7ff60 Fix error when updating installation with useMasterKey (#2888)
* Add failing test for updating installations with masterKey

* Prevent auth.installationId from being used when using masterKey

This allows masterKey to update any installation object
Fixes ParsePlatform/parse-server##2887
2016-10-19 15:06:19 -04:00
Eugene Antropov 305b037176 Vkontakte Auth: Change users.get to secure.checkToken (#2880)
* Change users.get to secure.checkToken

You can't get user info by client token due vk restrictions. You must check token via secure.checkToken.

* Configuration checks for vk auth.

* Move config check to promise, remove debug log, add message to logger on error.
2016-10-19 10:38:01 -04:00
Jeremy Louie b88b0c578f Fix multiple use of notEqualTo (#2882)
* Add failing test for multiple .notEqualTo on relation with same class

* Fix multiple .notEqualTo on relations with the same class

Multiple  should use the union of all objectIds not the intersect
Fixes ParsePlatform/parse-server#1596
2016-10-18 16:44:47 -04:00
Florent Vilmart d8ba9e8b7d Adds validation for id_token and access_token (#2878)
* ADds validation for id_token and access_token

* nit
2016-10-17 12:44:24 -04:00
Greenkeeper 60d506615d chore(package): update cross-env to version 3.1.3 (#2870)
https://greenkeeper.io/
2016-10-17 10:28:35 -04:00
Kulshekhar Kabra de36d9640b Allow configuration options for Postgres (#2873)
* Allow configuration options for Postgres

* Fix the use of incorrect options object.

* Refactor and test the postgres config parser.

* Remove unnecessary try/catch

* Remove unnecessary try/catch

* Add blank line at the end of the test file

* Rename file for consistency purposes
2016-10-15 17:29:54 -04:00
Kulshekhar Kabra 7af320932a Use the Postgres Adapter with a Postgres URI (#2871)
* Use the Postgres Adapter with a Postgres URI

* Handle malformed databaseURI
2016-10-15 10:22:49 -04:00
Florent Vilmart c253daa9c9 Update bootstrap.sh (#2862)
* Update bootstrap.sh

Fix issue when generating config.json

* Update bootstrap.sh

Fix mongodb URI
2016-10-13 13:05:36 -04:00
Greenkeeper 09f871b786 chore(package): update cross-env to version 3.1.1 (#2824)
https://greenkeeper.io/
2016-10-07 09:36:48 -04:00
Greenkeeper c0c516271c chore(package): update lodash to version 4.16.4 (#2835)
https://greenkeeper.io/
2016-10-07 09:36:11 -04:00
Greenkeeper 2cea0030f3 chore(package): update nodemon to version 1.11.0 (#2839)
https://greenkeeper.io/
2016-10-07 09:36:04 -04:00
Florent Vilmart 6979bb430b Properly handle select/include pairs #2786 (#2809)
* Regression test for 2786

* Fix issue affecting selcting/including keys
2016-10-07 09:33:55 -04:00
Greenkeeper d8f0f9e158 chore(package): update lodash to version 4.16.2 (#2779)
https://greenkeeper.io/
2016-09-27 08:53:24 -04:00
Greenkeeper 27b135b1c2 chore(package): update parse to version 1.9.2 (#2788)
https://greenkeeper.io/
2016-09-27 08:53:17 -04:00
Florent Vilmart 954a8a4159 Better support for null values in arrays (#2777)
* Adds reproduction for #2752

* Make sure we support null values in arrays of pointers for inclusion
2016-09-26 13:26:06 -04:00
Florent Vilmart a60935dbfb Changelog for 2.2.22 (#2775)
* Changelog for 2.2.22

* Update CHANGELOG.md
2016-09-24 18:36:45 -04:00
Greenkeeper 509f90b937 chore(package): update cross-env to version 3.0.0 (#2772)
https://greenkeeper.io/
2016-09-24 15:25:30 -04:00
Florent Vilmart 300150e50f Fix expports problem (#2774) 2016-09-24 15:25:22 -04:00
Florent Vilmart b5af4d017c Makes sure pointer includes are interpolated as get (#2747)
* test for repro #2005

* Adds ability to override CLP op from RestQuery.execute

* nits
2016-09-24 14:54:31 -04:00
Florent Vilmart 9260e8b05f Bumps minimum engine to 4.5 (#2733) 2016-09-24 13:53:31 -04:00
Florent Vilmart 2ec2bb5660 Removes dependency upon babel-polyfills (#2731)
* Removes runtime dependency babel-polyfill (#2692)

* Removes runtime dependency babel-polyfill

* removes references to polyfilled array includes

* Better support for polyfilling

* Removes unnecessary log

* Adds killswitch if tests are polyfilled

* Reverts usage of includes on strings
2016-09-24 13:53:15 -04:00
Florent Vilmart 3ba6e613b3 Adds repro for #1931 (#2735) 2016-09-24 13:53:04 -04:00
Florent Vilmart 4dcefd6386 Repro for issue #1944 (#2736) 2016-09-24 13:47:44 -04:00
Florent Vilmart 9c522be00d Support for nested .select() calls (#2737)
* Reproduction for #1567

* Recursive handling of nested pointer keys in select

* Better support for multi-level nested keys

* Adds support for selecting columns natively (mongo)

* Support for postgres column selections

* Filter-out empty keys for pg
2016-09-24 13:43:49 -04:00
Florent Vilmart 4974dbea37 Adds ability to ask for installation directory (#2746) 2016-09-24 13:42:51 -04:00
Florent Vilmart d4593931db Makes sure we plumb auth.installationId when updating installations (#2739) 2016-09-24 13:42:36 -04:00
Florent Vilmart 2183b84565 CLI for parse-live-query-server (#2765)
* adds CLI for parse-live-query-server, adds ability to start parse-server with live-query server

* Don't crash when the message is badly formatted
2016-09-24 13:34:05 -04:00
Arthur Cinader a41cbcbc7f Move password masking out of logging clients where possible (#2762)
Move password masking functionality into LoggerController.

The is a more aggresive approach to masking password string in the logs.

Cleaning the url is still in the PromiseRouter because picking it out of the log string
would be fragile.

This will cause more log messages to be scanned for password strings, and may cause a password
string to be obsfucated that is not neccesarily part of parse internals -- but i think that is
still a good thing....

see: #2755 & #2680
2016-09-22 15:05:54 -04:00
Florent Vilmart ad707457be Obfuscate password name value pairs in log strings (#2755)
* Unit test to catch password in logs.

* Add clean to logger controller to "look for" password strings in log messages.
2016-09-20 21:45:24 -04:00
Greenkeeper 5feceaa6d6 chore(package): update lodash to version 4.16.0 (#2742)
https://greenkeeper.io/
2016-09-19 15:15:10 -04:00
Florent Vilmart e30989c7d3 Lookup for email in username field to match docs if email is undefined (#2732)
* Lookup for email in username field to match docs if email is undefined

* Adds support for sendMail option to when email is selected

* Proper does not exists clause
2016-09-18 18:32:34 -04:00
Florent Vilmart 7e037ffa72 version 2.2.21 (#2730) 2016-09-18 13:03:18 -04:00
Florent Vilmart 0ec1e8ca7f Revert "Removes runtime dependency babel-polyfill" (#2729) 2016-09-18 12:47:42 -04:00
Florent Vilmart e97579ea85 Adds changelog for 2.2.20 (#2724)
* Adds changelog for 2.2.20

* nit
2016-09-18 07:55:37 -04:00
Jose Luis e5fa3b5e27 I only add a line in the readme for a new SMTP adapter for send emails (#2726) 2016-09-17 23:42:47 -07:00
Greenkeeper 56f3e63112 chore(package): update request to version 2.75.0 (#2725)
https://greenkeeper.io/
2016-09-17 19:06:42 -04:00
Florent Vilmart 263ca5e052 Adds CloudCode handler for beforeFind (#2715)
* Adds CloudCode handler for beforeFind

- Allows cloud code to modify a query before it is run
- Works with promises for a safer environment
- Supports modifiying the current query
- Supports issuing new queries

* Adds test for cornercase empty queries from rest

* Makes sure restOptions is always definied
2016-09-17 13:52:35 -07:00
Florent Vilmart ddb0fb8a27 Adds redis cache for distributed environments (#2691)
* Makes schemaCache clearning promise-based

* Adds redis cache adapter for distributed systems

* Adds redis service to travis

* allow pg to fail
2016-09-17 13:52:02 -07:00
Florent Vilmart f9dca6072a Travis: Makes sure the global success is reported on build leader (#2723)
* Adds exporting vars after success

* Echo var

* remove echo
2016-09-17 16:46:56 -04:00
Florent Vilmart ba0e529f81 Build merges on master to lastest branch (#2722)
* Build merges on master to lastest branch

* Only on master
2016-09-17 15:57:51 -04:00
Florent Vilmart 109bc9a0ce handling matching api.parse.com when calling upgradeToRevocableSession without a sessionToken (#2721) 2016-09-17 12:52:52 -07:00
Florent Vilmart 90e9994195 Removes runtime dependency babel-polyfill (#2692)
* Removes runtime dependency babel-polyfill

* removes references to polyfilled array includes
2016-09-17 12:50:04 -07:00
Florent Vilmart 97c124bbbe Supports boolean values in cluster options (#2689) 2016-09-17 12:45:12 -07:00
Lama Chandrasena 22c1a87d80 Saved filename fixed so that clients display it correctly #2415 (#2705)
* increased filename length to 36 to make sure clients display correct filename

* increased filename length to 36 to make sure clients display correct filename

* added uuid prefix to saved filenames
2016-09-17 14:27:31 -04:00
Greenkeeper 4c927dd898 chore(package): update winston-daily-rotate-file to version 1.3.1 (#2706)
https://greenkeeper.io/
2016-09-17 13:31:33 -04:00
Greenkeeper 408764a7b7 chore(package): update mongodb to version 2.2.10 (#2714)
https://greenkeeper.io/
2016-09-17 13:08:35 -04:00
Greenkeeper 86a0f1ad02 chore(package): update jasmine to version 2.5.2 (#2719)
https://greenkeeper.io/
2016-09-17 12:14:36 -04:00
Steven Shipton 0773523bc2 Ensure _acl is updated when _rperm and _wperm updated (#2701)
* Ensure _acl is updated when _rperm and _wperm updated

* alternative solution

* Only try to apply $set for permission updates
2016-09-17 11:05:33 -04:00
Mathias Rangel Wulff f6312a1b98 Correct check for undefined variable (#2693)
The code was comparing the result of `typeof` with a variable named `undefined`.

As typeof returns a string it should compare to `'undefined'`
2016-09-10 17:32:10 -04:00
Florent Vilmart cab69b4913 Changelog 2.2.19 (#2686)
* Changelog 2.2.19

* Bumps package.json
2016-09-09 18:20:40 -04:00
Florent Vilmart ac348f5c4f Reverts calling next() after handling response (#2634)
* Revert "Makes sure routes don't overlap and yield a header set error"

* removes next() calls in PromiseRouter

* Reverts calling next() after response

* Adds fail calls when next() calls traverse tests
2016-09-09 17:28:41 -04:00
Florent Vilmart 7f6fb11d78 Revocable session upgrade only on mongo (#2685)
* Revert "Tries a new travis configuration"

* Revocable session upgrade only on mongo
2016-09-09 17:02:15 -04:00
Florent Vilmart cb7b54941b Direct Access to parse-server (#2316)
* Adds ParseServerRESTController experimental support

* Adds basic tests

* Do not create sessionToken when requests come from cloudCode #1495
2016-09-09 15:18:37 -04:00
Florent Vilmart ccf2b14b98 Updates Travis configuration (#2645) 2016-09-09 14:48:34 -04:00
Florent Vilmart 340eb46fe1 Adds endpoint for non-revocable session token upgrade (#2646) 2016-09-09 14:48:06 -04:00
Florent Vilmart c5fdd91aa3 Makes sure we don't duplicate user ACL's keys (#2651)
* Adds repro for issue #2246

* Provide fix for issue #2246

* Nit with Set to deduplicate the acl array

* remove debuging console.log
2016-09-09 14:43:59 -04:00
Florent Vilmart e8aa1ad312 Fix null pointer includes (#2657)
* Adds failing test for #2189

* Improves support for null values in includes

* nit
2016-09-09 14:41:21 -04:00
Florent Vilmart fc576cb415 Adds more expressive schema mismatch errors (#2662) 2016-09-09 14:41:11 -04:00
Florent Vilmart 364604e181 Run Schema validations after beforeSave #2672 (#2677)
* Adds test to repro #2672

* Run schema validation after beforeSave is run

* Makes authData part of base _User object

* exclude flaky pg test
2016-09-09 14:41:03 -04:00
BAm Wang 58a2ee32fa Validate username password type when logging in, and validate email when reseting password (#2679)
* Validate username password type when logging in, and validate email when reset password

* Add test for validation
2016-09-09 11:24:33 -04:00
Florent Vilmart eb1cfcf351 Fix atom is undefined (#2663) 2016-09-08 21:19:39 -04:00
Yuki Takeichi fe62e92aa1 Add NullCacheAdapter (#2636)
* Add NullCacheAdapter

* clear returns promise explicitly

* Add NullCacheAdapter accessor
2016-09-07 08:08:09 -04:00
Florent Vilmart 33e3993a37 Fixes issue where info logs would appear in error logs (#2647)
* Reproduction of #2639

* Make sure the error level options is enforced
2016-09-04 12:07:40 -04:00
Greenkeeper 5676a6fbda chore(package): update pg-promise to version 5.3.3 (#2648)
https://greenkeeper.io/
2016-09-03 10:43:32 -04:00
Diwakar Cherukumilli 28bd37884d Adds ability to set an account lockout policy (#2601)
* Adds ability to set account lockout policy

* change fit to it in tests
2016-09-02 20:00:47 -04:00
Flavio Torres f6516a1d1e change log level to debug for classes without subscribers (#2594) (#2637) 2016-09-02 14:29:05 -04:00
Brage G. Staven 9d2d208609 Fixed 'undefined' when getting request header (#2635) 2016-09-01 08:12:30 -04:00
Florent Vilmart ac11e03e24 Removes Flaky PG Tests from test suite (#2632)
Removes some tests from running in the suite as they fail randomly on Travis
2016-09-01 00:09:28 -04:00
Florent Vilmart be9fb435dd Revert "Update jasmine to version 2.5.0 🚀" (#2631) 2016-08-31 21:04:21 -04:00
Greenkeeper 05805a6a70 chore(package): update jasmine to version 2.5.0 (#2624)
https://greenkeeper.io/
2016-08-31 11:16:06 -04:00
Michael Vogt e0c0608457 remove non referenced dependency, parse-server-simple-mailgun-adapter will include it (#2625) 2016-08-31 11:15:38 -04:00
Florent Vilmart 26ce63513c Removes mongodb v2.6.11 from test suite (#2619) 2016-08-30 11:30:08 -04:00
Florent Vilmart f727e8dd4e updates dev dependencies (#2618) 2016-08-30 10:33:56 -04:00
garethx 3e5b90ca3d Add HyperDev to providers list (#2617) 2016-08-30 09:49:56 -04:00
Florent Vilmart 10ace495d8 Adds jobs endpoint protected by masterKey (#2560)
* Adds jobs endpoint protected by masterKey

* Adds connection timeout for 15 minutes in jobs

* Refactors pushStatusHandler into StatusHandler

* Adds reporting of _JobStatus

* Only accept strings as messages

* Adds test for masterKey basic auth

* Adds CloudCodeRouter for cloud_code endpoint of job status, enable Jobs feature on dashboard

* xit racing test
2016-08-30 07:19:21 -04:00
Greenkeeper 4b2a780e03 chore(package): update pg-promise to version 5.3.2 (#2557)
https://greenkeeper.io/
2016-08-29 21:48:44 -04:00
Florent Vilmart 4d78446cb9 Adds --cluster support (#2596)
* Adds --cluster support

* Restart failing child processes
2016-08-29 21:43:35 -04:00
Greenkeeper d65f4a6f75 chore(package): update mongodb to version 2.2.9 (#2608)
https://greenkeeper.io/
2016-08-29 21:17:36 -04:00
Tom J 2aa14adf87 Only allow basic auth credentials with a known appId (#2574)
* Only allow basic auth credentials with a known appId

* Update middlewares.js

* Updating basic auth tests to use valid appId
2016-08-25 13:04:23 -04:00
Nurdaulet 8eafe45664 vk.com provider registered (#2579)
* vk.com auth data manager implemented

* vk.com provider registered

* Removed all the unnecessary changes

* Revert "Removed all the unnecessary changes"

This reverts commit 7dec629742.

* Revert "vk.com provider registered"

This reverts commit 25705d3d12.

* Reverted all the unnecessary changes

* vk.com auth manager bugs fixed
2016-08-25 13:03:48 -04:00
Greenkeeper a99bd82c43 chore(package): update parse-server-push-adapter to version 1.1.0 (#2588)
https://greenkeeper.io/
2016-08-25 13:03:36 -04:00
Nurdaulet 307a725884 vk.com auth data manager implemented (#2578) 2016-08-24 12:39:46 -04:00
Dharam Gollapudi 5ca4844bef Fix a typo (#2563)
Update connectionPrefix with collectionPrefix
2016-08-22 12:52:56 -04:00
Florent Vilmart 23e47f2e28 Makes sure routes don't overlap and yield a header set error (#2559) 2016-08-21 13:57:12 -04:00
Florent Vilmart 9ab488b6a0 Postgres: $all, $and CLP and more (#2551)
* Adds passing tests

* Better containsAll implementation

* Full Geopoint support, fix inverted lat/lng

* Adds support for $and operator / PointerPermissions specs

* Fix issue updating CLPs on schema

* Extends query support

* Adds RestCreate to the specs

* Adds User specs

* Adds error handlers for failing tests

* nits

* Proper JSON update of AuthData

* fix for #1259 with PG

* Fix for Installations _PushStatus test

* Adds support for GlobalConfig

* Enables relations tests

* Exclude spec as legacy

* Makes corner case for 1 in GlobalConfig
2016-08-20 16:07:48 -04:00
Drew e1de9f3a12 Changelog 2.2.18 (#2558) 2016-08-20 10:16:22 -04:00
Greenkeeper 7fa3c9916c chore(package): update winston-daily-rotate-file to version 1.3.0 (#2547)
https://greenkeeper.io/
2016-08-19 18:12:32 -04:00
Greenkeeper 0fd6ce35e2 chore(package): update parse-server-s3-adapter to version 1.0.5 (#2536)
https://greenkeeper.io/
2016-08-19 18:11:51 -04:00
Florent Vilmart 3a08ec9ce8 Adds bcrypt native binding for better login performance (#2549)
* Adds bcrypt native binding for better login performance

* Swaps bcrypt-nodejs for bcryptjs as compatible with bcrypt native

* Fixes package versions
2016-08-19 13:53:57 -07:00
Greenkeeper 9429659d90 chore(package): update mongodb to version 2.2.7 (#2554)
https://greenkeeper.io/
2016-08-19 16:40:02 -04:00
Arthur Cinader 5f67caefde Make parse-server cloud code logging closer parse.com legacy (#2550)
* Make parse-server cloud code logging much to parse.com legacy. (fixes #2501)

1. More closely mimic the wording.  Include the user id.

2. Truncate input and result at 1k char.

3. Use more sensible metadata that would makes sense to index.  The guideline I used was: if it makes sense to filter on, put it in metadata.  If it makes sense to "free text" search on, then put it in the message.

  - file and console output, logging an object does not do what on might expect.  For example, logging a function's "params":

  ```
  expected:

    info: Ran cloud function aFunction for user qWHLVEsbEe with:
      Input: {"foo":"bar","bar":"baz"}
      Result: "it worked!" functionName=aFunction, params= { foo: "bar", "bar": baz }, user=qWHLVEsbEe

  what you actually get:

    info: Ran cloud function aFunction for user qWHLVEsbEe with:
      Input: {"foo":"bar","bar":"baz"}
      Result: "it worked!" functionName=aFunction, foo=bar, bar=baz, user=qWHLVEsbEe
  ```
  - logging highly variable metadata is pretty useless for indexing when logs are sent to a logging repository like elastic search.  In that use case, you want to index stuff you expect to filter on like user, hook type.

  - finally, putting the same input and result data in both the metadata and the message makes each message much larger with no additional value (that I know of anyway :).

4. Change some of the naming of functions in trigger.js to make future work easier.  I was confused about why there were three logging functions in trigger and it took me awhile to get that before hooks and after hooks are logged differently.  I just changed the names to make it obvious at first glance.

5. Add some try/catches to help any future futzers  see syntax errors, etc instead of just hanging.

Some log examples from unit test output:

```
info: Ran cloud function loggerTest for user YUD2os1i5B with:
  Input: {}
  Result: {} functionName=loggerTest, user=YUD2os1i5B

info: beforeSave triggered for MyObject for user nssehQ3wtz:
  Input: {}
  Result: {} className=MyObject, triggerType=beforeSave, user=nssehQ3wtz

info: afterSave triggered for MyObject for user XdznQgTD0p:
  Input: {"createdAt":"2016-08-19T01:11:31.249Z","updatedAt":"2016-08-19T01:11:31.249Z","objectId":"POoOOLL89U"} className=MyObject, triggerType=afterSave, user=XdznQgTD0p

error: beforeSave failed for MyObject for user 7JHqCZgnhf:
  Input: {}
  Error: {"code":141,"message":"uh oh!"} className=MyObject, triggerType=beforeSave, code=141, message=uh oh!, user=7JHqCZgnhf

info: Ran cloud function aFunction for user YR3nOoT3r9 with:
  Input: {"foo":"bar"}
  Result: "it worked!" functionName=aFunction, user=YR3nOoT3r9

error: Failed running cloud function aFunction for user Xm6NpOyuMC with:
  Input: {"foo":"bar"}
  Error: {"code":141,"message":"it failed!"} functionName=aFunction, code=141, message=it failed!, user=Xm6NpOyuMC

info: Ran cloud function aFunction for user CK1lvkmaLg with:
  Input: {"longString":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus lobortis semper diam, ac euismod diam pharetra sed. Etiam eget efficitur neque. Proin nec diam mi. Sed ut purus dolor. Nulla nulla nibh, ornare vitae ornare et, scelerisque rutrum eros. Mauris venenatis tincidunt turpis a mollis. Donec gravida eget enim in luctus.\n\nSed porttitor commodo orci, ut pretium eros convallis eget. Curabitur pretium velit in odio dictum luctus. Vivamus ac tristique arcu, a semper tellus. Morbi euismod purus dapibus vestibulum sagittis. Nunc dapibus vehicula leo at scelerisque. Donec porta mauris quis nulla imperdiet consectetur. Curabitur sagittis eleifend arcu eget elementum. Aenean interdum tincidunt ornare. Pellentesque sit amet interdum tortor. Pellentesque blandit nisl eget euismod consequat. Etiam feugiat felis sit amet porta pulvinar. Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\nNulla faucibus sem ipsum, at rhoncus diam pulvinar at. Vivamus consectetur, diam... (truncated)
  Result: {"longString":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus lobortis semper diam, ac euismod diam pharetra sed. Etiam eget efficitur neque. Proin nec diam mi. Sed ut purus dolor. Nulla nulla nibh, ornare vitae ornare et, scelerisque rutrum eros. Mauris venenatis tincidunt turpis a mollis. Donec gravida eget enim in luctus.\n\nSed porttitor commodo orci, ut pretium eros convallis eget. Curabitur pretium velit in odio dictum luctus. Vivamus ac tristique arcu, a semper tellus. Morbi euismod purus dapibus vestibulum sagittis. Nunc dapibus vehicula leo at scelerisque. Donec porta mauris quis nulla imperdiet consectetur. Curabitur sagittis eleifend arcu eget elementum. Aenean interdum tincidunt ornare. Pellentesque sit amet interdum tortor. Pellentesque blandit nisl eget euismod consequat. Etiam feugiat felis sit amet porta pulvinar. Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\nNulla faucibus sem ipsum, at rhoncus diam pulvinar at. Vivamus consectetur, diam... (truncated) functionName=aFunction, user=CK1lvkmaLg
```

* Implement PR comments:
- add back params to metadata and add back to the test
- use screaming snake case for conts

* fix typo
2016-08-19 16:39:51 -04:00
Greenkeeper 277df54dc8 chore(package): update pg-promise to version 5.3.1 (#2519)
https://greenkeeper.io/
2016-08-19 11:31:02 -04:00
Florent Vilmart 9ecb9a3595 Postgres: Operations, Hooks, OAuth login, Files support (#2528)
* Adds files related tests through fs-adapter with PG

* Schema deletions implementations

* Adds Hooks spec

* Fix test

* Adds support for containsAll (numbers and strings)

* Better support for deleteFields and deleteClass

* Recursive JSON update for authData

* Adds node_modules to travis cache

* Disable temporarily to make tests pass

* Adds _perishable_token support for _User class

* ignore when a table creation fails at init (table exists)

* Adds support for AddUnique and Remove

* PG 9.4 compatible functions

* Re-enable tests

* nit

* Better handling of schema creation race
2016-08-18 18:05:26 -04:00
Marko Matić 3164b478ea Syncing afterSave/afterDelete trigger calls (Issue #2489) (#2499)
* Implemented syncing afterSave/afterDelete trigger calls with REST request execution flow (Issue 2489). After this change, afterSave and afterDelete triggers CAN return a promise, which needs to be resolved inside a trigger for REST request flow to continue. If trigger doesn't return a promise, request flow continues.

* Added {} to multiline if.

* Fixed bad commit.

* Fixed problem with beforeSave triggers becoming async.
2016-08-17 09:26:42 -04:00
Emilie Montredon 430ae378f2 Updated README.md (#2538)
Created a new email adapter to use Mailjet with parse-server to send email verification and reset password emails.
2016-08-17 09:26:18 -04:00
Arthur Cinader 7616b8d3ec Fix capitalization, typo, and grammar mistake (#2533)
* Fix capitalization, typo, and grammar mistake

* fix two typos.
2016-08-16 16:29:34 -04:00
Héctor Ramos fc73518b98 Update ISSUE_TEMPLATE.md 2016-08-15 14:07:50 -07:00
Arthur Cinader 24a2416d10 fix typo (#2525) 2016-08-15 16:49:45 -04:00
Florent Vilmart c0249283ac Advancements with postgres (#2510)
* Start DB runner from tests

* Connect GridstoreAdapter only when needed

* removes unused package

* better test errors reporting

* Adds support for __op.Delete

* Better test error reporting

* Makes sure all tests can run without crashing

* Use xdescribe to skip test suite

* Removes unused dependencies

* Let volatiles classes be created with PG on start

* Do not fail if class dont exist

* adds index.spec.js to the pg suite

* Use a new config each test to prevent side effects

* Enable EmailVerificationToken specs with pg

* Makes sure failure output is not cut

* Reduces number of ignored tests in ParseObject.spec

* Inspect reconfiguration errors

* Mark GlobalConfig is incompatible with PG

- Problem is with nested updates (param.prop = value)

* PG: Nested JSON queries and updates

- Adds support for nested json and . operator queries
- Adds debug support for PG adapter
- Adds loglevel support in helper

* Enable working specs in ParseUser

* Sets default logLevel in tests to undefined

* Adds File type support, retores purchaseValidation specs

* Adds support for updating jsonb objects

- Restores PushController tests

* Proper implementation of deleteByQuery and ORs

- Adds ParseInstallation spec to the test suite

* xit only failing tests

* Nit on ParseAPI spec

* add sorting operator

* properly bound order keys

* reverts describe_only_db behavior

* Enables passing tests

* Adds basic support for relations, upsertOneObject aliased to createObject

* progress on queries options

* Fix ACL update related problems

* Creates relation tables on class creation

* Adds Relation tests

* remove flaky tests

* use promises instead of CB

* disable flaky test

* nits

* Fixes on schema spec

- Next thing is to implemenet geopoint and files correctly

* fix failues

* Basic GeoPoint support

* Adds support for $nearSphere/$maxDistance geopoint queries

* enable passing tests

* drop tables afterEach for PG, clean up relation tables too

* Better initialization/dropTables
2016-08-15 16:48:39 -04:00
Florent Vilmart 2f1ee2186b Makes sure _acl is set as empty {} (#2495)
* regression test for #2465

* Makes sure _acl is set when locked down

- Fixes #2465
2016-08-12 13:15:55 -07:00
Brage G. Staven e690b73bb5 Stream video with GridStoreAdapter (implements byte-range requests) (#2437)
* Stream video with GridStoreAdapter

* fixing nits. Removing test(Range not accepted as header)

* nit

* Changed names. Added function to check if stream-requirements is fulfilled.
2016-08-12 15:58:18 -04:00
Greenkeeper 122c3fd1a3 chore(package): update lodash to version 4.15.0 (#2506)
https://greenkeeper.io/
2016-08-12 15:55:09 -04:00
Andrew Lane 80de86c8ae Implement Janrain Capture and Janrain Engage auth provider (#2436)
* Janrain engage auth provider
* Modeled after the existing providers in /src/authDataManager/ and also after https://github.com/janrain/Janrain-Sample-Code/blob/master/widget-examples/server.js
* See also: http://developers.janrain.com/overview/social-login/identity-providers/user-profile-data/#normalized-user-profile-data

* Janrain capture auth provider
* Modeled after the existing providers in /src/authDataManager/
* See also: https://docs.janrain.com/api/registration/entity/#entity

* Janrain engage auth provider
* Modeled after the existing providers in /src/authDataManager/ and also after https://github.com/janrain/Janrain-Sample-Code/blob/master/widget-examples/server.js
* See also: http://developers.janrain.com/overview/social-login/identity-providers/user-profile-data/#normalized-user-profile-data

* Janrain capture auth provider
* Modeled after the existing providers in /src/authDataManager/
* See also: https://docs.janrain.com/api/registration/entity/#entity

* Adding missing newlines at EOF
2016-08-12 13:34:24 -04:00
Florent Vilmart a5a172918e Refactor logging to provide common logger from LoggerAdapter (#2478)
* Refactor logging to provide common logger from LoggerAdapter

Move logger logic de WinstonLoggerAdapter

Further improvements in configuration

Use logger instead of getLogger

- Removes PLog module

Reverts name changes

nits

* Adds additional logging levels as requirements

* Adds tests for logging configuration

* removes flaky test

* investigate...

* further investigation

* Adds silent option to disable console output

* Restores logs with VERBOSE in tests

* Expose controller instead of adapter, reduces method requirements for adapter

* Shuffles initializations around

* Fix doc

* Load cloudCode last to make sure the logger is available

* Adds test to make sure we can load an adapter from npm module

* extract defaults

* Adds defaultMongoURI to defaults

* fix defaults values

* Proper error for PG failures

* Disable flaky test
2016-08-12 10:25:24 -07:00
CongHoang 6e0a25dea0 fix export Bytes data type to JSON (#2409) 2016-08-09 12:05:46 -04:00
Antoine Lenoir 452887bd05 Change pushHash with stringified data (#2397)
* Change pushHash with stringify data

* stringify data.alert for pushHash

* stringify data.alert if object for pushHash

* test push notification when data.alert is an object
2016-08-09 12:00:56 -04:00
MBDeveloper e6d31a07ab Add function name to the request object - this can help in generic way to log the function calls. (#2309) 2016-08-09 11:28:57 -04:00
Felix Dumit a87ab6f1c4 Support for validating LinkedIn SDK access_tokens (#2486) 2016-08-09 08:38:22 -04:00
Greenkeeper cbcd44bd72 chore(package): update lodash to version 4.14.2 (#2485)
https://greenkeeper.io/
2016-08-08 22:13:06 -04:00
Greenkeeper 1a90405fad chore(package): update request-promise to version 4.1.1 (#2483)
https://greenkeeper.io/
2016-08-08 11:58:14 -04:00
Greenkeeper b6b09e68de chore(package): update babel-core to version 6.13.2 (#2471)
https://greenkeeper.io/
2016-08-07 20:03:11 -07:00
Florent Vilmart fc3ebd0bd0 Style improvements (#2475)
* HooksRouter is enabled by default

* Adds middleswares on PromiseRouter, fixes #2410

* Move testing line to helper

* Modernize middlewares.js

* Moves DB uniqueness initialization to DBController, modernize

* Moves testing related code to spec folder

* remove unused _removeHook function

* Adds tests, docs for Analytics and improvements

* nit

* moves back TestUtils
2016-08-07 20:02:53 -07:00
Drew ae36200d1f Disable tests that don't work in Postgres, and don't start a mongo for postgres tests (#2481)
* Disable tests that don't work in Postgres, and don't start a mongo for postgres tests

* Remove unnecessary chages

* remove more unnecessary chagnes
2016-08-07 22:21:01 -04:00
Jeremy Louie f27dff0ae6 Handle queries with equalTo on objectId and relation conditions (#2472)
* Add test for notEqualTo on relation with equalTo on objectId

* Properly handles queries with equalTo on objectId and relation conditions

This is done by converting shorthand $eq condition to $eq condition instead of clobbering.
2016-08-07 18:58:32 -07:00
Greenkeeper cdb46dcb2e chore(package): update babel-preset-es2015 to version 6.13.2 (#2473)
https://greenkeeper.io/
2016-08-07 18:48:24 -07:00
Greenkeeper 55690254de chore(package): update babel-register to version 6.11.6 (#2400)
https://greenkeeper.io/
2016-08-07 18:48:04 -07:00
Vitaly Tomilov 39078e8a2b Improving use of query methods. (#2353)
Improving use of query methods.
2016-08-06 13:24:42 -04:00
Arthur Cinader fa736f1df7 Allow logger to add transports (#2363)
- Move all of the winston logic from FileLoggerAdapter to WinstonLoggerAdapter
- Export WinstonLoggerAdapter so it can be sublcassed
- Expost the ability to add adittional transports to logger
- Import FirehoseLoggerAdapter alongside other adapters so it can be configured.
2016-08-06 08:29:19 -04:00
Greenkeeper 36891f4ed7 chore(package): update babel-polyfill to version 6.13.0 (#2459)
https://greenkeeper.io/
2016-08-05 12:54:09 -04:00
Greenkeeper 27cfa9645a chore(package): update babel-preset-es2015 to version 6.13.1 (#2463)
https://greenkeeper.io/
2016-08-05 12:53:57 -04:00
Greenkeeper e41e3c3202 chore(package): update babel-core to version 6.13.1 (#2462)
https://greenkeeper.io/
2016-08-05 12:28:18 -04:00
Andrew Lane 1a661b73a6 Fix copy/paste error in linked in auth provider (#2468) 2016-08-05 12:11:05 -04:00
Florent Vilmart d79b478e0b Revert "Update babel-preset-es2015 to version 6.13.0 🚀" (#2469) 2016-08-05 12:10:53 -04:00
sud e4c2e65263 Non trivial before save pointer clobber (#2406)
* test case to check beforeSave changes clobbers fetched pointer fields
Basically if beforeSave makes any changes to the object it is trying to save, the fetched pointer fields on the client gets clobbered to only pointer.

* propogate only changed fields to response.
Earlier we were returning all fields even if any changes happened in beforeSave. This causes the fetched pointer fields on the client to get clobbered to only pointers.
This fix returns only the changed fields thus avoiding pointer clobber.

* The goal of this comparision seems to be checking that the all returns the user correctly.
Also it is consistent with the hosted parse that user.username not returned from PUT request.
2016-08-05 08:53:54 -04:00
Greenkeeper eb1b2eb83f chore(package): update babel-plugin-syntax-flow to version 6.13.0 (#2457)
https://greenkeeper.io/
2016-08-04 20:36:18 -07:00
Greenkeeper f293b1d0a3 chore(package): update babel-preset-es2015 to version 6.13.0 (#2460)
https://greenkeeper.io/
2016-08-04 20:35:55 -07:00
Greenkeeper 01df251847 chore(package): update gaze to version 1.1.1 (#2461)
https://greenkeeper.io/
2016-08-04 20:35:47 -07:00
Greenkeeper 3044a382d6 chore(package): update babel-polyfill to version 6.9.1 (#2454)
https://greenkeeper.io/
2016-08-04 20:35:37 -07:00
Greenkeeper 62caea6c28 chore(package): update parse to version 1.9.1 (#2452)
https://greenkeeper.io/
2016-08-04 17:00:47 -07:00
Steven Shipton 4dda71698b Fix test:win for windows. Move coverage from test:win to coverage:win. Fix test on windows bash (#2453) 2016-08-04 23:18:52 +02:00
Greenkeeper f5755e8f73 chore(package): update multer to version 1.2.0 (#2449)
https://greenkeeper.io/
2016-08-04 12:13:38 -07:00
Dharam Gollapudi 115fd7278c Remove unused imports (#2403)
Remove the unused imports, as they are now part of logger.js
2016-08-04 10:31:03 -07:00
Dharam Gollapudi eee11a7acb Remove unused imports (#2404)
Remove unused imports, as it is now part of ClassesRouter.js
2016-08-04 10:30:00 -07:00
Steven Van Bael 8d8192bbcc Add Content-Length header to HTTP response in FilesRouter (#2408) 2016-08-04 10:29:10 -07:00
Greenkeeper 5865d59b5a chore(package): update request-promise to version 4.1.0 (#2426)
https://greenkeeper.io/
2016-08-04 10:27:58 -07:00
Florent Vilmart 946f8a3872 fix typo when logging result (#2443) 2016-08-04 10:27:47 -07:00
Greenkeeper f1c9d97d2f chore(package): update pg-promise to version 5.2.7 (#2433)
https://greenkeeper.io/
2016-08-03 16:44:31 +02:00
Greenkeeper 3dbf0b22f4 chore(package): update babel-runtime to version 6.11.6 (#2401)
https://greenkeeper.io/
2016-07-29 13:04:43 -07:00
Greenkeeper ddf8fd432b chore(package): update pg-promise to version 5.2.6 (#2402)
https://greenkeeper.io/
2016-07-29 13:04:35 -07:00
Greenkeeper 39c267cacb chore(package): update mongodb to version 2.2.5 (#2418)
https://greenkeeper.io/
2016-07-29 13:04:17 -07:00
bcomeau a957df0a91 Add parse-server-mailgun-adapter-template to README.md (#2413)
Customize the e-mail sent to your users when they reset their password or when we verify their email address.
2016-07-27 15:01:12 -07:00
Greenkeeper 831b29b015 chore(package): update lodash to version 4.14.0 (#2376)
https://greenkeeper.io/
2016-07-26 07:54:37 -07:00
Greenkeeper da94a1e8be chore(package): update winston-daily-rotate-file to version 1.2.0 (#2386)
https://greenkeeper.io/
2016-07-26 07:54:26 -07:00
Greenkeeper 72cfec0e0c chore(package): update nodemon to version 1.10.0 (#2388)
https://greenkeeper.io/
2016-07-26 07:54:12 -07:00
Florent Vilmart fa49f05fdd Changelog 2.2.17 (#2374)
* Version 2.2.17

* remove generated by line
2016-07-23 23:05:46 +02:00
Florent Vilmart fdd60a4a30 Revert "Version 2.2.17" (#2373) 2016-07-23 23:04:54 +02:00
Florent Vilmart 1cf23a3431 Version 2.2.17 (#2372)
* Version 2.2.17

* [skip ci] remove generated by line
2016-07-23 22:59:54 +02:00
Florent Vilmart 15a07859b0 Make sure _PushStatus operations are run in order (#2367) 2016-07-23 12:20:04 -07:00
Florent Vilmart 7964d0a999 Adds support for multiple twitter auths options (#2256)
* Adds support for multiple twitter auths options

* Adds user test
2016-07-23 12:02:45 -07:00
Greenkeeper fc42388305 chore(package): update babel-register to version 6.9.0 (#2371)
https://greenkeeper.io/
2016-07-23 12:02:23 -07:00
Florent Vilmart c9fc80984a Makes sure we don't strip authData or session token from users using masterKey (#2348)
* Makes sure we don't strip auth data or session token from users queried with masterKey (#2342))

* nit: test title
2016-07-23 11:14:53 -07:00
Greenkeeper 88d913f3a2 chore(package): update babel-core to version 6.11.4 (#2341)
https://greenkeeper.io/
2016-07-23 11:13:11 -07:00
Florent Vilmart 3b2da1d734 Cloud code logs (#2370)
* Adds logging (info/error) when running cloudCode triggers

* Adds logs for cloud-function calls

- Match parse.com original logging
2016-07-23 11:10:06 -07:00
Simonas Karuzas 93b41170fd LiveQuery constrains matching fix (#2357)
* Removing sessionToken and authData from _User objects included in a query

This bug caused sessionToken to be replaced on client side to some old
sessionToken from DB.

* Removing dangling variable that is never used

* Checking if object has defined key for Pointer constraints in liveQuery

If there is a liveQuery subscription, with Pointer type constrains (e.g
query.equalTo('user', Parse.User.current())), and new object has
undefined value for that field, we get this error:

    error: Uncaught internal server error. [TypeError: Cannot read
property 'className' of undefined] TypeError: Cannot read property
'className' of undefined
    at matchesKeyConstraints
(…/node_modules/parse-server/lib/LiveQuery/QueryTools.js:145:51)

* LiveQuery constrains matching fix

When constrains == null, we get “Uncaught internal server error”
2016-07-22 21:27:52 -07:00
Andrew Lane a60c3f48ce Typo fix for error message when can't ensure uniqueness of user email addresses (#2360) 2016-07-22 21:27:01 -07:00
Greenkeeper b799ec7bc3 chore(package): update pg-promise to version 5.2.5 (#2366)
https://greenkeeper.io/
2016-07-22 21:25:03 -07:00
Greenkeeper 499c78a922 chore(package): update request to version 2.74.0 (#2365)
https://greenkeeper.io/
2016-07-22 21:24:33 -07:00
Florent Vilmart 09bd9e3b2c Adds schema caching capabilities (5s by default) (#2286)
* Adds schema caching capabilities (off by default)

* Use InMemoryCacheAdapter

* Uses proper adapter to generate a cache

* Fix bugs when running disabled cache

* nits

* nits

* Use options object instead of boolean

* Imrpove concurrency of loadSchema

* Adds testing with SCHEMA_CACHE_ON

* Use CacheController instead of generator

- Makes caching SchemaCache use a generated prefix
- Makes clearing the SchemaCache clear only the cached schema keys
- Enable cache by default (ttl 5s)
2016-07-22 21:23:59 -07:00
Greenkeeper 66c4b98b55 chore(package): update babel-cli to version 6.11.4 (#2339)
https://greenkeeper.io/
2016-07-21 22:32:22 -07:00
Greenkeeper 921617ad40 chore(package): update pg-promise to version 5.2.3 (#2334)
https://greenkeeper.io/
2016-07-21 22:32:09 -07:00
Andrew Lane 397e6bc976 Fix typo in logging for commander parseConfigFile (#2352) 2016-07-20 16:08:22 -04:00
Arthur Cinader ac1f762f07 Fix minor typos in test names (#2351) 2016-07-20 12:32:18 -04:00
Diwakar Cherukumilli ab3b4addbf updates usage of setting: emailVerifyTokenValidityDuration (#2331) 2016-07-20 09:29:32 -04:00
Blayne Chard c8e1ab851f Run next() after successfully sending data to the client (#2338)
This allows developers to add express handlers for successful completion of parse-server requests.
ie Analytics or Logging.
2016-07-20 09:28:20 -04:00
Marek Mikołajczyk d1a6caeee3 Allow usage of analytics adapter (#2327)
* Allow usage of analytics adapter

* Use promises in controller
2016-07-20 09:27:15 -04:00
Florent Vilmart 02edf27f05 Restores code coverage (#2340) 2016-07-19 23:33:01 -04:00
Florent Vilmart dfac9725dd Update Mongodb client to 2.2.4 (#2329)
* chore(package): update mongodb to version 2.2.4

https://greenkeeper.io/

* fix for mongo 2.2.4

* Delete schema promise on error...

- Just because this way we can let the adapter an opportunity to yield a new error....
OR A SUCCESS

* Fixes test

* Restore fix
2016-07-19 21:40:01 -04:00
Florent Vilmart 78ea02968a Cache all the mongodb/version folder (#2336)
* Cache all the version folder

* list cache
2016-07-19 21:27:04 -04:00
Florent Vilmart 8719afdc1b Parses correctly Parse.Files and Dates when sent to Cloud Code Functions (#2297)
* fix for #2294

* fail tests

* Makes sure dates are compatible with Parse.com CloudCode #2214

* Adds regression tests for #2204
2016-07-18 23:14:32 -07:00
Florent Vilmart 4f89ec39cd Uses one query for installation dedup (#2281) 2016-07-18 23:12:29 -07:00
Diwakar Cherukumilli 6f292059ba Adds ability to expire email verify token (#2216) 2016-07-18 23:10:36 -07:00
Florent Vilmart 033bc317e6 Report validation errors with correct error code (#2299) 2016-07-18 23:03:43 -07:00
Greenkeeper 40c4fcdebf chore(package): update parse-server-s3-adapter to version 1.0.4 (#2318)
https://greenkeeper.io/
2016-07-18 23:02:01 -07:00
Dan Huang c81f48aae6 don't serve null authData values (#2320) 2016-07-18 23:00:16 -07:00
Florent Vilmart 025482ea1a Fix null relation problem (#2319)
* Add null check for relation type map.

For relations that are not explicitly defined in the schema, we need a null check here.

* Making change to force rebuild.

* Reverting change.

* Adds test
2016-07-18 22:58:37 -07:00
Florent Vilmart f310e6678e Fix flaky tests (#2324)
* Fix flaky test (There were open connections to the server left after the test finished)

* fix test that's happening on the end of the minute

* remove focus testing
2016-07-18 21:43:34 -07:00
Greenkeeper 2ba724938f chore(package): update request-promise to version 4.0.1 (#2311)
https://greenkeeper.io/
2016-07-18 22:36:46 -04:00
Mihai Iorga 676d2e28de pushStatusHandler wrong payload md5, PushController send pushId to adapter also (#2067) 2016-07-18 19:39:28 -04:00
Florent Vilmart bae19f2c76 Clear the connectionPromise upon close or error (#2314) 2016-07-18 11:51:50 -04:00
Greenkeeper 0f49f314b2 chore(package): update request-promise to version 4.0.0 (#2298)
https://greenkeeper.io/
2016-07-15 18:53:01 -07:00
Vitaly Tomilov fa96f0c389 Adding proper generic Not Implemented. (#2292)
Proper way to generate common Promise rejects.
2016-07-15 20:32:06 -04:00
Greenkeeper d07dd4c49f chore(package): update pg-promise to version 5.2.0 (#2296)
https://greenkeeper.io/
2016-07-15 16:01:23 -07:00
Sen Palanisami 7d234e054b Log objects rather than JSON strings and option for single line logs (#2028)
* Log objects rather than JSON strings and option for single line logs

This reverts commit fcd914bdfd.

* Better password stripping tests
2016-07-15 16:18:50 -04:00
Greenkeeper 514095dc35 chore(package): update pg-promise to version 5.1.4 (#2291)
https://greenkeeper.io/
2016-07-15 09:49:06 -07:00
Florent Vilmart 069275d3df Fix for #1840, Strip operations from results, forwards delete operations to SDKs (#1946)
* Adding a test demonstrating issue #1840.

* Fixes #1840

* Adds failing test with other use case

- That test fails on parse.com as well

* Bumps parse to 1.9.0

* exclude pg db

* Exclude pg on other test

* Adds clientSDK compatibility check for forward deletion

- Mark js1.9.0 as compatible

* Strips all operations from result

- fix for #1606
2016-07-15 09:24:53 -04:00
Ran Hassid fcfe2a0376 add digits oauth provider (#2284) 2016-07-15 09:23:42 -04:00
Blayne Chard 1125c88f56 Adding request headers to cloud functions fixes #1461 (#2274) 2016-07-13 19:41:05 -04:00
Greenkeeper bf645233b0 chore(package): update cross-env to version 2.0.0 (#2271)
https://greenkeeper.io/
2016-07-13 12:10:53 -04:00
Florent Vilmart 9bf21ef093 Restores ability to include non pointer keys (#2263)
- Matches the behaviour on parse.com
- fixes #2262
2016-07-13 09:14:54 -04:00
Florent Vilmart 32f7230aca Creates a new sessionToken when updating password (#2266)
* Creates a new sessionToken when updating password

* Adds test ensuring email is properly sent when upgrading from anon
2016-07-13 07:18:24 -04:00
Greenkeeper f1ff9fecce chore(package): update pg-promise to version 5.1.3 (#2257)
https://greenkeeper.io/
2016-07-12 19:41:09 -07:00
Nikita Lutsenko d1426c19dd Add Gitter chat link to the README. (#2264)
* Add Gitter chat link to the README.

Closes #1700

* Update README.md
2016-07-12 21:18:28 -04:00
Greenkeeper a2f8a1d2b3 chore(package): update winston-daily-rotate-file to version 1.1.5 (#2248)
https://greenkeeper.io/
2016-07-12 12:31:06 -07:00
Nikita Lutsenko 18235352c4 Merge pull request #2259 from ParsePlatform/client-sdk-info
Exposes the ClientSDK infos if available
2016-07-12 11:17:52 -07:00
Magda Wiśniewska 3c1da3cc2a Allow next middleware handle error in handleParseErrors (#2260) 2016-07-12 12:32:10 -04:00
Greenkeeper 868ad5bdfd Update parse to version 1.9.0 🚀 (#2233)
* chore(package): update parse to version 1.9.0

https://greenkeeper.io/

* Removes Hooks client API as provided by node SDK
2016-07-12 10:09:16 -04:00
Florent Vilmart 2498a954ce Exposes clientSDK into RestQuery, RestWrite and rest 2016-07-12 10:06:13 -04:00
Florent Vilmart a040aafcd8 Adds parser for SDK version 2016-07-12 09:20:00 -04:00
Valery Vaskabovich 7a2e906202 validate_purchase fix for SANDBOX requests (#2253)
* Fixed routing for validate_purchase method

* Fixed validate_purchase endpoint
2016-07-11 19:38:42 -04:00
Drew 9658d216f3 Version 2.2.16 (#2247) 2016-07-10 21:00:55 -07:00
Alaa Attya Mohamed 4634bfb3e4 Fixed: When using an explicit database adapter, you must also use and explicit filesAdapter. (#2245) 2016-07-10 10:16:42 -07:00
OzgeAkin 6dc8d287d6 Option to ignore parse file storage (#2222)
* flag in configurations to use only files adapter

* added lib folder

* remove lib

* alignment edit

* replace comma with semicolon

* ignore parse file storage if fileKey is not provided (undefined)
2016-07-10 10:15:30 -07:00
Greenkeeper 1fb7679b22 chore(package): update pg-promise to version 5.1.2 (#2243)
https://greenkeeper.io/
2016-07-09 12:54:04 -07:00
Greenkeeper 83f686355e chore(package): update request to version 2.73.0 (#2242)
https://greenkeeper.io/
2016-07-09 10:53:14 -07:00
Drew 6ca1351b8f Revert "Update PostgresStorageAdapter.js" (#2241) 2016-07-08 22:52:14 -07:00
Vitaly Tomilov a92b23b97d Update PostgresStorageAdapter.js (#2093)
If it really needs to reject with `undefined`, then this is the right way. Usually one just returns the result, let the caller provide the `.catch` ;)

This way real errors can be swallowed, not very good ;)
2016-07-08 22:51:22 -07:00
Florent Vilmart a55466fde8 Fixes #2221: Nested Or queries (#2231)
* Adds repro for #2221

* Fixes nested or queries

* not for PG yet
2016-07-08 22:03:13 -07:00
Florent Vilmart 61aa5a8d62 Let auth data be updated on login (#2219)
* Let user update authData token upon login

* Adds tests that ensures linked authData isnt overriden

* fixes focused testing problem
2016-07-08 21:49:46 -07:00
Eric Raio 4e9e817a80 Adding amazon ses adapter to README (#2226)
We've been using this parse server amazon SES email adapter and would like to share with the rest of the community.
2016-07-07 11:32:05 -04:00
Blayne Chard 8338783075 Adding more information to the permission denied error (#2210) 2016-07-07 11:30:29 -04:00
Greenkeeper 944e5addd3 chore(package): update babel-istanbul to version 0.11.0 (#2174)
https://greenkeeper.io/
2016-07-05 12:49:06 -07:00
Greenkeeper 9de347872f chore(package): update babel-istanbul to version 0.10.0 (#2173)
https://greenkeeper.io/
2016-07-05 12:48:46 -07:00
Greenkeeper 5bf878be22 chore(package): update pg-promise to version 5.1.0 (#2177)
https://greenkeeper.io/
2016-07-05 12:44:04 -07:00
Tyler Brock 2f2ff377eb Better e-mail adapter testing (#2208) 2016-07-05 12:08:46 -07:00
Greenkeeper 48b33d3889 chore(package): update babel-istanbul to version 0.9.0 (#2172)
https://greenkeeper.io/
2016-07-05 07:45:13 -07:00
Diwakar Cherukumilli 08c63f324a Adds ability to prevent login with unverified emails (#2175) 2016-07-04 13:56:35 -04:00
Andrew Lane b641712d4d Improve error message for Parse.Error.INCORRECT_TYPE failure condition (#2185) 2016-07-02 06:11:35 -07:00
Steven Shipton d1cdea00af Expose InMemoryCacheAdapter. Update tests (#2186) 2016-07-02 06:08:13 -07:00
Tyler Brock d599fdd4fe Changelog 2.2.15 (#2183) 2016-07-01 08:08:51 -07:00
Greenkeeper 04dc2d2233 chore(package): update pg-promise to version 5.0.5 (#2170)
https://greenkeeper.io/
2016-06-29 12:03:01 -07:00
Greenkeeper d2098d7ee4 chore(package): update pg-promise to version 5.0.4 (#2168)
https://greenkeeper.io/
2016-06-29 12:01:49 -07:00
Andrew Lane 9e725f616b Fix typo in description for Parse.Error.INVALID_QUERY (#2164) 2016-06-28 19:40:25 -07:00
Tyler Brock 799e59618c Stop requiring verifyUserEmails for password reset functionality (#2166) 2016-06-28 22:25:44 -04:00
Greenkeeper a73f150a1a chore(package): update pg-promise to version 5.0.3 (#2156)
https://greenkeeper.io/
2016-06-27 13:15:55 -07:00
Greenkeeper a2474fa0f1 chore(package): update ws to version 1.1.1 (#2139)
https://greenkeeper.io/
2016-06-26 22:25:01 -07:00
David Keita 19080ed7e7 #506 fix: Deleting a file does not delete from fs.files (#1758)
* #506 fix: Deleting a file does not delete from fs.files

* test added to check that GridStoreAdapter deleteFile removes from fs.files and fs.chunks
2016-06-27 00:40:43 -04:00
Greenkeeper b7d15cc37f chore(package): update gaze to version 1.1.0 (#2144)
https://greenkeeper.io/
2016-06-27 00:38:18 -04:00
Vitaly Tomilov 1f2f831cbe Update PostgresStorageAdapter.js (#2094)
this isn't just a simplification, but also fixing a bug: the result of `count(*)` is always returned as a string that needs conversion, not as a number, because it is 64-bit, and all 64-bit numbers are returned as strings. So, the previous `=== 0` would have never worked.
2016-06-26 23:56:01 -04:00
Drew e4cfe5af24 Kill without validation (#2089)
* remove WithoutValidation from config and push

* remove one use of WithoutValidation

* remove another WithoutValidation

* Kill WithoutValidation and skipValidation
2016-06-26 23:50:40 -04:00
Florent Vilmart 147b493e23 Results invalid session when providing an invalid session token (#2154)
* Results invalid session when providing an invalid session token

* Reverts unsafe loggers

* Fixes failing tests

- The tests were failin when run in sequence as we called done() before the JSSDK had a chance to register the session token, therefore having a proper logout call in afterEach
2016-06-26 23:20:02 -04:00
Hussam Moqhim a861c4e506 Fix issue #2134: creating an anonymous user (#2155)
* fix issue #2134

* username is returned only when generated

* fix unit tests

* incorporate @flovilmart review
2016-06-26 23:19:37 -04:00
Greenkeeper 2a4fbf7a15 chore(package): update pg-promise to version 5.0.2 (#2151)
https://greenkeeper.io/
2016-06-26 12:26:12 -07:00
Glenn Reyes 98d1ae808a Add postmark email adapter alternative (#2146)
* Add postmark email adapter alternative

* Link postmark adapter to npm package

* Link mandrill adapter to npm package

* List email adapters
2016-06-25 16:14:07 -04:00
Florent Vilmart c32ed52f04 Changelog 2.2.14 (#2148) 2016-06-25 15:22:06 -04:00
Florent Vilmart 3e413683f7 Makes HTTPResponse serializable (#2143)
* Use the callback body instead of response.body that may not be set

* Adds test to handle undefined responses

* Adds toJSON method to properly serialize HTTPResponse

* Use ES5 defineProperty to make keys enumerable

* removes body key from serialization

* Indent nits
2016-06-25 10:56:02 -07:00
Greenkeeper 35b4c063fb chore(package): update pg-promise to version 4.8.0 (#2129)
https://greenkeeper.io/
2016-06-23 09:25:17 -07:00
Greenkeeper 29ffbabd3d chore(package): update babel-core to version 6.10.4 (#2119)
https://greenkeeper.io/
2016-06-21 18:53:56 -07:00
Greenkeeper 57cecd800c chore(package): update body-parser to version 1.15.2 (#2107)
https://greenkeeper.io/
2016-06-21 12:54:16 -07:00
Greenkeeper c36ca0c44e chore(package): update pg-promise to version 4.7.5 (#2111)
https://greenkeeper.io/
2016-06-21 10:59:28 -07:00
Nikita Lutsenko a96b0afac5 Merge pull request #2118 from ParsePlatform/greenkeeper-babel-core-6.9.1
Update babel-core to version 6.9.1 🚀
2016-06-21 10:36:42 -07:00
greenkeeperio-bot 1feedef21d chore(package): update babel-core to version 6.9.1
https://greenkeeper.io/
2016-06-21 09:54:14 -07:00
Drew 006540cda5 Implement pointer types in postgres. (#2086)
* Support pointers in postgres

* implement count

* Fix bug in createClass
2016-06-17 11:09:42 -07:00
Drew ab06055369 Postgres exclude failing tests (#2081)
* reload the right data

More passing postgres tests

Handle schema updates, and $in for non array columns

remove authdata from user and implement ensureUniqueness

Make some tests work, detect existing classes

Throw proper error for unique index violation

fix findOneAndUpdate

Support more types

support more type

Support boolean, fix _rperm/_wperm, add TODO

Support string types and also simplify tests

Move operator flattening into Parse Server and out of mongo adapters

Move authdata transform for create into Parse Server

Move authdata transforms completely in to Parse Server

Fix test setup

inline addSchema

Inject default schema to response from DB adapter

* Mark tests that don't work in Postgres

* Exclude one more test

* Exclude some more failing tests

* Exclude more tests
2016-06-17 12:59:16 -04:00
Vitaly Tomilov 7da4debbe0 Update PostgresStorageAdapter.js (#2087)
1. Deleting tables in a transaction, as opposed to just a task.
2. Added transaction where it was supposed to be. However, it is not enough, the logic is still broken there...

First, do not use `.catch` before `.then`. It is dangerous without good understanding how it actually works.

Check this out:
```js
        .catch(error => {
          if (error.code === PostgresRelationDoesNotExistError) {
            return this.createClass(className, {fields: {[fieldName]: type}}) // this gets into the following `.then`
          } else if (error.code === PostgresDuplicateColumnError) {
            // Column already exists, created by other request. Carry on to
            // See if it's the right type.
           
// this will get the following `.then` with `undefined` as the value

          } else {
            throw error;
          }
        })
```
2016-06-17 00:04:58 -07:00
Drew c37a4ea1a1 Incorporate vitaly-t changes (#2085) 2016-06-16 19:34:00 -07:00
Drew 9d078003a6 Pin pg-postgres version (#2084) 2016-06-16 18:44:31 -07:00
Vitaly Tomilov a685ed8814 Update package.json (#2083)
upgrading pg-promise version.
2016-06-16 18:43:08 -07:00
Drew 0ff35e18f0 Add more postgres support (#2080)
* reload the right data

More passing postgres tests

Handle schema updates, and $in for non array columns

remove authdata from user and implement ensureUniqueness

Make some tests work, detect existing classes

Throw proper error for unique index violation

* fix findOneAndUpdate
2016-06-16 15:39:05 -07:00
Drew 1a75101146 Fix flaky test (#2078)
* Debug flaky test

* create new object instead of modifying and assigning existing object

* use getOneSchema instead of this.data when updating fields

* Remove debug stuff

* Don't try to validate existing fields

* run just one test

* Verbose test all

* Use schema instead of this.data

* Switch to all tests
2016-06-16 15:34:33 -07:00
Greenkeeper 627b4164bb chore(package): update express to version 4.14.0 (#2079)
https://greenkeeper.io/
2016-06-16 10:39:13 -07:00
Fosco Marotto d1f77a9a98 Merge pull request #2053 from drew-gross/postgres-3
Continue with postgres support
2016-06-16 10:19:07 -07:00
Greenkeeper 35ed09a7b4 chore(package): update redis to version 2.6.2 (#2076)
https://greenkeeper.io/
2016-06-16 08:53:30 -07:00
Blayne Chard 2eb861db66 Fixing typo in _loadRoles which prevents caching of roles. (#2063) 2016-06-15 22:22:23 -07:00
Drew ae0ba7b5a9 Pin packages (#2073)
* Pin all package versions

Closes #2040

* Pin packages to a working version

* Drop extra stuff
2016-06-15 23:28:07 -04:00
Drew 726c866a4d Remove console.log 2016-06-15 17:25:17 -07:00
Drew f7f5bab75d Remove attempt to fix tests 2016-06-15 17:24:00 -07:00
Drew 9ed3fcc536 Remove duplicate test 2016-06-15 17:23:29 -07:00
Drew Gross 8130658583 Fix purge 2016-06-15 15:01:21 -07:00
Drew Gross f23d80ec9a Ignore coverage requirements for postgres adapter until I can figure out how to run coverage on it. 2016-06-15 13:11:51 -07:00
Drew Gross 510a890a03 centralize mongo schema handling logic 2016-06-15 13:11:51 -07:00
Drew Gross a69a88f3a4 Fix some stuff 2016-06-15 13:11:51 -07:00
Drew Gross f796d69d19 Move $options validation into Parse Server 2016-06-15 13:11:51 -07:00
Drew Gross 452b737be4 WIP 2016-06-15 13:11:51 -07:00
Drew Gross 634d672ad1 passing another test 2016-06-15 13:11:51 -07:00
Drew Gross 803b9be97a Add unique indexing for username/email
WIP

Notes on how to upgrade to 2.3.0 safely

index on unique-indexes: c454180 Revert "Log objects rather than JSON stringified objects (#1922)"

reconfigure username/email tests

Start dealing with test shittyness

most tests passing

Make specific server config for tests async

Fix more tests

Save callback to variable

undo

remove uses of _collection

reorder some params

reorder find() arguments

finishsh touching up argument order

Accept a database adapter as a parameter

First passing test with postgres!

Fix tests

Setup travis

sudo maybe?

use postgres username

reorder find() arguments

Build objects with default fields correctly

Don't tell adapter about ACL

WIP

Passing postgres test with user

Fix up createdAt, updatedAt, nad _hashed_password handling
2016-06-15 13:11:51 -07:00
Drew Gross 2cafd6919f Passing postgres test with user 2016-06-15 13:11:51 -07:00
Drew Gross f75c8b3a4d index on unique-indexes: c454180 Revert "Log objects rather than JSON stringified objects (#1922)"
reconfigure username/email tests

Fix broken cloud code

Save callback to variable

undo

Fix all tests where connections are left open after server closes.

Fix issues caused by missing gridstore adapter

remove uses of _collection

reorder find() arguments

Accept a database adapter as a parameter

sudo maybe?

use postgres username

reorder find() arguments

Build objects with default fields correctly

Don't tell adapter about ACL

WIP
2016-06-15 13:11:51 -07:00
Lemuel Barango 693707b33c fixed typo in comment (#2070) 2016-06-15 13:09:20 -07:00
Florent Vilmart 39f11a5134 Update .travis.yml (#2065)
Runs coverage no matter what build result.
2016-06-14 20:10:48 -07:00
Marco Cheung b2a5092627 Endpoint for purging all objects in class (#2032)
* Endpoint for purging all objects in class

* Use deleteObjectsByQuery

* Standalone handling function and purge cache

* Change endpoint url
2016-06-14 15:52:49 -07:00
Marco Cheung d9f8486790 Fix role test (#2057) 2016-06-14 10:17:15 -07:00
Héctor Ramos 5838a49396 Move logging docs to Advanced configuration (#1895) 2016-06-13 13:05:33 -07:00
Greenkeeper 63e2ceeb02 chore(package): update deepcopy to version 0.6.3 (#2049)
https://greenkeeper.io/
2016-06-13 09:36:50 -07:00
Marco Cheung 7c8ad7e071 Throw error when push is missing configuration (#2035) 2016-06-13 09:26:02 -07:00
Marco Cheung 0ec78d478b Fix checking existent class for allowClientClassCreation (#2051) 2016-06-13 09:21:52 -07:00
Drew 2cc1b0cfa9 Remove unused function (#2046) 2016-06-13 07:34:57 -04:00
Vitaly Tomilov 324cd85775 Update PostgresStorageAdapter.js (#2045)
1. removing `return` where it does nothing;
2. changing repeated queries to be executed as a task. See [Chaining Queries](https://github.com/vitaly-t/pg-promise/wiki/chaining-queries).
2016-06-12 17:27:02 -07:00
Drew 5518edc2a5 Postgres adapter (#2012)
* Remove adaptiveCollection

* Remove an adaptiveCollection use

* Remove an adaptiveCollection

* make adaptiveCollection private

* Remove collection from mongoadapter

* Move schema collection usage into mongo adapter

* stop relying on mongo format for removing join tables

* reduce usage of schemaCollection

* remove uses of _collection

* Move CLP setting into mongo adapter

* remove all uses of schemaCollection

* make schemaCollection private

* remove transform from schemaCollection

* rename some stuff

* Tweak paramaters and stuff

* reorder some params

* reorder find() arguments

* finishsh touching up argument order

* Accept a database adapter as a parameter

* First passing test with postgres!

* Actually use the provided className

* index on unique-indexes: c454180 Revert "Log objects rather than JSON stringified objects (#1922)"

* Start dealing with test shittyness

* Make specific server config for tests async

* Fix email validation

* Fix broken cloud code

* Save callback to variable

* undo

* Fix tests

* Setup travis

* fix travis maybe

* try removing db user

* indentation?

* remove postgres version setting

* sudo maybe?

* use postgres username

* fix check for _PushStatus

* excludes

* remove db=mongo

* allow postgres to fail

* Fix allow failure

* postgres 9.4

* Remove mongo implementations and fix test

* Fix test leaving behind connections
2016-06-12 16:35:13 -07:00
Drew d559cb2382 Move transform acl (#2021)
* Move ACL transforming into Parse Server

For the database adapters, it will be more performant and easier to work with _rperm and _wperm than with the ACL object. This way we can type it as an array and so on, and once we have stronger validations in Parse Server, we can type it as an array containing strings of length < x, which will be much much better in sql databases.

* Use destructuring
2016-06-12 13:39:41 -07:00
Drew 5baa53d858 Changelog 2.2.13 (#2039) 2016-06-12 13:06:08 -07:00
Drew 729a0c101d Pin deepcopy (#2038) 2016-06-12 15:59:23 -04:00
Yaroslav Syrytsia afbc662dcd fix gettings user roles from cache (#2034) 2016-06-12 02:43:11 -07:00
Drew 7e868b2dcc Unique indexes (#1971)
* Add unique indexing

* Add unique indexing for username/email

* WIP

* Finish unique indexes

* Notes on how to upgrade to 2.3.0 safely

* index on unique-indexes: c454180 Revert "Log objects rather than JSON stringified objects (#1922)"

* reconfigure username/email tests

* Start dealing with test shittyness

* Remove tests for files that we are removing

* most tests passing

* fix failing test

* Make specific server config for tests async

* Fix more tests

* fix more tests

* Fix another test

* fix more tests

* Fix email validation

* move some stuff around

* Destroy server to ensure all connections are gone

* Fix broken cloud code

* Save callback to variable

* no need to delete non existant cloud

* undo

* Fix all tests where connections are left open after server closes.

* Fix issues caused by missing gridstore adapter

* Update guide for 2.3.0 and fix final tests

* use strict

* don't use features that won't work in node 4

* Fix syntax error

* Fix typos

* Add duplicate finding command

* Update 2.3.0.md
2016-06-10 20:27:21 -07:00
Michael Chan 6415a35433 Update google.js (#2023)
Updated the authentication method of Google Sign-in (from v1 to v3)
2016-06-10 16:25:25 -07:00
Dan Huang 11301d9590 Fix an issue in MongoTransform where transforming object type fields with null values fails (#2029) 2016-06-10 13:44:41 -07:00
Drew a00d795404 Changelog 2.2.12 (#2019)
* Changelog 2.2.12

* Update CHANGELOG.md
2016-06-09 18:41:02 -07:00
Marco Cheung 2e50416cba Recursive parse date in cloud function (#2014)
* Recursive parse date in cloud function

* Use _.mapValues
2016-06-09 12:37:05 -07:00
Drew fcd914bdfd Revert "Log objects rather than JSON stringified objects" (#1995) 2016-06-08 10:11:08 -07:00
Patrick Pelletier db76aa376f Merge pull request #2001 from skinp/legacy-files
Support file objects in the legacy bucket: files.parse.com
2016-06-07 18:36:24 -07:00
Patrick Pelletier 65518cdd64 Support file objects in the legacy bucket: files.parse.com 2016-06-07 15:59:45 -07:00
Florent Vilmart e7e2369132 Fixes for Class Level and Pointer Permissions (#1989)
* Fixes for Pointer Permissions

- Fix bug that would leave public CLP when setting a new set of permissions
- Sets empty permissions if missing to match parse.com API
- Updates tests to reflect changes

* Adds regression test for #1991

* Fit -> It
2016-06-06 09:31:50 -07:00
Manuel ac705a8da6 Fixed 3rd party OAuth link (#1986)
Fixed bad OAuth link in wiki [ci-skip]
2016-06-04 12:37:40 -04:00
Drew aeb6880b85 Allow installation get (#1980) 2016-06-04 12:37:15 -04:00
Sen Palanisami bea655e61b Log objects rather than JSON stringified objects (#1922) 2016-06-01 18:35:54 -07:00
Tyler Brock abeecad907 Ensure beforeSave hook response is object before stripping (#1969) 2016-06-01 12:01:35 -07:00
Florent Vilmart 38f02af511 Update .travis.yml (#1967)
Fixes tags not running when having more than a digit
2016-06-01 13:49:53 -04:00
Jeremy Pease c6c9c97b54 Allow custom error codes with response.error from Cloud Code functions and before/after hooks (#1955) 2016-06-01 10:28:06 -04:00
Drew 103839ce60 Changelog 2.2.11 (#1962) 2016-05-31 17:01:10 -07:00
Tyler Brock 04172eda5a Strip read only fields from serialized beforeSave response objects (#1964) 2016-05-31 17:00:41 -07:00
Florent Vilmart 5176efbe16 Patch failing delete (#1960)
* Patch failing delete

* Fixing unit test
2016-05-31 13:42:56 -07:00
Blayne Chard 3997b1aa5a Fixing #1900 JS SDK file upload (#1935)
* Fixing #1900 JS SDK file upload

JS SDK file upload uses req.body._ContentType to specify the upload content type

* Fixing import statements

* Dont clear the cache just delete the new entry that the test added.

* adding E2E test for _ContentType support
2016-05-30 21:42:45 -04:00
Drew cd525802a6 Remove mongoFind and mostly remove adaptiveCollection (#1924)
* Use adapter.count

* use adapter.upsertOneObject

* Use adapter.deleteObjectsByQuery

* Use adapter.find

* use adapter.find

* Update tests to avoid mongoFind

* Fix a test to not use mongoFind

* Fix a test to not use mongoFind

* remove some mongoFind

* Remove some mongoFind

* Remove some mongoFind

* Remove more mongoFind

* remove more mongoFind

* remove more mongoFind

* remove more mongoFind

* remove more mongoFind

* remove more mongoFind

* remove more mongoFind

* remove more mongoFind

* remove more mongoFind

* Restore update ios device token with duplicate device token to original

* remove a mongoFind

* remove a mongoFind

* formatting

* formatting

* remove a mongoFind

* remove a mongoFind

* remove a mongoFind

* kill mongoFind

* Fix tests

* Fix tests

* fix syntax

* Fix test
2016-05-28 12:25:09 -04:00
Tyler Brock 17374eff8d Round out webhookKey support in cli, docs, and Config (#1936) 2016-05-28 12:21:55 -04:00
Madhav Bhagat 181b12771d Make beforeSave work when object is passed to success (#1937) 2016-05-28 12:21:22 -04:00
Drew 1c162101be Fix database URI (#1911)
* Fix database URI

* Use database name that is less likely to collide with people's actual databases
2016-05-28 02:27:59 -07:00
Emad Ehsan 7d2929abe2 Changed deprecated res.send(status) to res.sendStatus(status) (#1912) 2016-05-28 02:26:53 -07:00
Tyler Brock 1487a73f16 Fix expected webhook external response format (#1934) 2016-05-27 19:04:49 -07:00
Drew 50b9ae4803 Fix #1919 (#1926) 2016-05-26 19:15:54 -04:00
Tyler Brock c5e3be8b64 Strip objectId out of responses from HooksController (#1925) 2016-05-26 15:33:37 -07:00
Jeremy Pease dd1d18dbe6 Set push.storedPushData in serverInfo features if push is available (#1923)
It's assumed that when push is available, the pushHandler is used and data is saved in _PushStatus.
This allows Parse Dashboard push history to be enabled: ParsePlatform/parse-dashboard#378
2016-05-26 14:02:13 -07:00
Tyler Brock 2561987d20 Add webhookKey support (#1920) 2016-05-26 11:17:24 -07:00
Drew 0850c184d3 Fixes #1649 (#1650)
* Regression test #1649

* Address comments

* Comment

* Change emails to help debug flaky test failures

* More logging info to debug flaky tests
2016-05-25 19:48:18 -04:00
Héctor Ramos 98f8db36e8 Update issue template (#1899)
Make it clearer what kind of information we are looking for.
2016-05-25 19:33:57 -04:00
Drew c9a138d9ca Break schemaController dependency. (#1901)
* Break dependency on MongoCollection for updateMany

* Move transformWhere usage into MongoTransform

* Pass parse schema into transformUpdate

* break dependency on schemaController

* remove schema parameter

* move key name validation up one level

* Move validation out of mongo adapter

* Move validation into Parse Server and transformUpdate in Mongo Adapter

* Update mongo adapter

* Use adapter API

* use and fix mongo adapter api

* Remove/rename stuff

* Kill transform in DBController

* better imports for transform

* Tidy ConfigRouter

* Remove schemaController in more places

* Remove comment
2016-05-24 17:21:20 -07:00
Peter J. Shin c4499d25e0 Merge pull request #1888 from drew-gross/finish-moving-query-logic
Finish moving query logic
2016-05-24 16:38:14 -07:00
Drew Gross 0896f33824 Remove unnecessary null check 2016-05-24 16:30:43 -07:00
Jeremy Pease 4864518315 Update schema mismatch error to include type string (#1898) 2016-05-24 13:52:45 -07:00
Drew Gross 3ed3c7b62f Move more mongo specific stuff into mongo adapter 2016-05-23 21:16:03 -07:00
Drew Gross 05ae010b91 Kill mongoOptions 2016-05-23 20:28:12 -07:00
Drew Gross d428041a83 Consistent parameter order 2016-05-23 20:22:04 -07:00
Drew Gross a763f7c2fc move transformWhere into mongo adapter 2016-05-23 20:19:03 -07:00
Drew Gross 135b0e0254 Remove options from count 2016-05-23 20:15:51 -07:00
Drew Gross e444ca8425 Can't sort a count 2016-05-23 20:12:03 -07:00
Drew Gross aa072dabff Remove limit from count 2016-05-23 20:09:59 -07:00
Drew Gross c9be5a3aac Destructure mongo options 2016-05-23 20:07:16 -07:00
Drew Gross cf0a4b246f remove adaptive collection call 2016-05-23 20:04:10 -07:00
Drew Gross 14938bbe7a create + use adapter count instead of collection count 2016-05-23 19:00:58 -07:00
Drew Gross 1ae1d42c32 transformWhere in MongoAdapter 2016-05-23 18:39:34 -07:00
Drew Gross 4052470820 remove schemaController paramater 2016-05-23 18:39:34 -07:00
Drew Gross 7dca7e20b0 Remove schemaController parameter 2016-05-23 18:39:34 -07:00
Drew Gross e440046be4 Remove getRelationFields 2016-05-23 18:39:34 -07:00
Drew Gross f4b1f7b951 Remove all dependencies on schemaController 2016-05-23 18:39:34 -07:00
Drew Gross d944255e4e remove another use of schemaController 2016-05-23 18:39:34 -07:00
Drew Gross a55b2b6209 remove another use of schemaController 2016-05-23 18:39:34 -07:00
Drew Gross 00de555ecb remove one use of schemaController 2016-05-23 18:39:34 -07:00
Drew Gross 474a893a22 Pass the Parse Schema into untransform 2016-05-23 18:39:34 -07:00
Drew Gross fe8160449c Revert changes to find 2016-05-23 18:39:34 -07:00
Drew Gross 74ee8613d8 Fix inner keys named _rperm, _wperm 2016-05-23 18:39:19 -07:00
Drew Gross c928dcc118 Prevent untransforming inner object keys named _p_* 2016-05-23 18:38:48 -07:00
Drew Gross 9f149e6db5 Don't mess with inner object keys called _auth_data_* 2016-05-23 18:38:16 -07:00
Drew Gross 69d5a2f87c Make find() in MongoStorageAdapter 2016-05-23 18:35:38 -07:00
Drew 3a96140103 Fix issue with pointers getting un-hydrated when there is a beforeSave (#1884)
* Add failing test and simplify RestWrite

* simplify and add test stubs

* Fix issue
2016-05-23 17:13:32 -07:00
Drew 614e1ac8e5 Move query logic into mongo (#1885)
* Move Parse Server logic into Parse Server and out of MongoAdapter

* Move untransforming up one level

* Make find() in MongoStorageAdapter

* Put nested object untransforming into it's own function

* Simplfy nested untransform

* Don't mess with inner object keys called _auth_data_*

* Prevent untransforming inner object keys named _p_*

* Fix inner keys named _rperm, _wperm

* Fix bugs with inner objects behaving strange when other fields have same name as key in specific circumstances

* remove params from untransform nested object

* Revert changes to find
2016-05-23 19:31:51 -04:00
Adrian Brink 88fa7bad92 Update README.md (#1886)
Add link to the parse-server-module repo for easier reference.
2016-05-23 15:49:06 -07:00
Peter J. Shin 6b28075194 Merge pull request #1834 from drew-gross/move-stuff
Move query format validation into Parse Server
2016-05-23 12:46:44 -07:00
Steven Shipton 8b43d2644f Change existsSync to exception handler (#1879) 2016-05-23 11:49:43 -07:00
Peter J. Shin 361d4a34ce Merge pull request #1876 from ParsePlatform/pin-mongodb
Pin mongodb package
2016-05-23 10:44:53 -07:00
Drew e93a1ad7fc Pin mongodb package
Fixes #1855
2016-05-23 09:15:32 -07:00
Drew 392102eb97 Cache users by objectID, and clear cache when updated via master key (fixes #1836) (#1844)
* Cache users by objectID, and clear cache when updated via master key

* Go back to caching by session token. Clear out cache by querying _Session when user is modified with Master Key (ew, hopefully that can be improved later)

* Fix issue with user updates from different sessions causing stale reads

* Tests aren't transpiled...

* Still not transpiled
2016-05-22 09:59:36 -07:00
Drew eefa2ccac7 Handle "bytes" type in DB. Fixes #1821. (#1866) 2016-05-22 09:33:59 -04:00
Jeremy Pease fab8cfdfc7 Add additional default fields to _Installation class (#1852)
Fields are appVersion, appName, appIdentifier, and parseVersion.
These fields are sent by Android and iOS SDKs.
2016-05-20 18:15:47 -07:00
Tyler Brock 9bad87905f Add MongoDB 3.2 to test matrix (#1842)
* Add MongoDB 3.2 to test matrix

 - Updated mongodb-runner to support specifying storage engine
 - Specifying mmapv1 explictly because of new 3.2 default

* Increase jasmine.DEFAULT_TIMEOUT_INTERVAL to 3 seconds

* Use fixed mongodb-runner

* Increase jasmine.DEFAULT_TIMEOUT_INTERVAL to 15 seconds

* Update to mongodb-runner 3.3.2 upon release
2016-05-20 14:18:41 -07:00
Drew b2183680be Refactor cloud code tests (#1837)
* Move cloud code tests

* Remove _removeHook calls that are no longer necessary

* Use Strict
2016-05-19 13:38:16 -07:00
benishak fece2a4b47 change logger.error to logger.info to prevent pm2 from crashing (#1830) 2016-05-19 09:52:44 -07:00
Hussam Moqhim 03108e6347 add support for http basic auth (#1706)
* add support for http basic auth

* update http auth per flovilmart feedback
2016-05-18 20:06:37 -07:00
Drew Gross c416cad43f remove extra special keys list 2016-05-18 18:59:52 -07:00
Drew Gross 5165c80570 Remove validate parameter from deleteObjectsByQuery 2016-05-18 18:56:47 -07:00
Drew Gross 643bdc8227 Move query validation out of mongo adapter 2016-05-18 18:56:47 -07:00
Drew Gross 15fc186a51 Extract query validation logic 2016-05-18 18:56:47 -07:00
Drew Gross ea09213511 lift query key validation out of transformWhere 2016-05-18 18:56:47 -07:00
Drew Gross 559205bc64 Lift no-query-ACL validation out of transformWhere 2016-05-18 18:56:47 -07:00
Drew Gross 66b8a8474e Lift query key validation out of transformQueryKeyValue 2016-05-18 18:56:47 -07:00
Drew Gross b24ff151d5 Correct validation of inner keys 2016-05-18 18:56:47 -07:00
Drew Gross 0d856c1f23 remove extra lines 2016-05-18 18:54:06 -07:00
Drew 4d4361451c Refactor MongoTransform.js (#1823)
* Split transformAtom into transfromTopLevelAtom and transformInteriorAtom

* Use single param for inArray and inObject

* Tidyness in transformKeyValue

* Add transformInteriorKeyValue

* Remove update from tranformInteriorKeyValue

* Split out transform update

* Move validation out of transfromUpdate

* Remove force paramater from transformTopLevelAtom throw error after if necessary

* Turn transformKeyValue into transfromKey since it is only used for that purpose

* Remove unnecessary stuff from transformKey

* convert transformKey to use parse format schema

* interior keys fixes

* Add test for interior keys with special names

* Correct validation of inner keys
2016-05-18 18:14:54 -07:00
Tyler Brock d7d4699832 Fix logic for missing geo index error message check (#1824) 2016-05-18 12:33:55 -07:00
Blayne Chard 8c09c3dae1 Adding Caching Adapter, allows caching of _Role and _User queries (fixes #168) (#1664)
* Adding Caching Adapter, allows caching of _Role and _User queries.
2016-05-18 12:12:30 +12:00
KartikeyaRokde 5d887e18f0 FIX #1572 - Accepting LOGS_FOLDER as env variable (#1757)
* FIX #1572 - Accepting LOGS_FOLDER as env variable

* Changed env variable LOGS_FOLDER to PARSE_SERVER_LOGS_FOLDER

* Added Note for starting parse-server with PARSE_SERVER_LOGS_FOLDER env variable
2016-05-17 17:02:28 -07:00
Marco Cheung 40965186c0 Mask sensitive information when logging (#1790) 2016-05-17 13:15:44 -07:00
Drew Gross b40e16647b Changelog for version 2.2.10 2016-05-16 16:39:41 -07:00
Tyler Brock 3b4ae2d0a0 Write old ACL format in _acl in addition to new format (#1810) 2016-05-16 14:41:25 -07:00
Tyler Brock 1854928fe7 Add test to ensure you can set ACL in beforeSave (#1772) 2016-05-13 18:17:22 -07:00
Drew e4998c256a Move field name validation logic out of mongo (#1752)
* Remove transformKey(...)

* Move validation logic into Parse Server and out of Mongo Adapter

* Fix nits
2016-05-13 15:28:14 -07:00
Peter J. Shin 4bfe2c5014 Merge pull request #1774 from drew-gross/pointer-perms-in-dash
Tell the dashboard that Pointer Permissions are implemented so it can let you edit them.
2016-05-13 14:47:24 -07:00
Marco Cheung d0c3535a39 Fix error when unset user email (#1778) 2016-05-13 10:51:01 -07:00
Drew Gross c2cfa14627 Tell dashboard that the feature exits 2016-05-12 16:30:38 -07:00
Marco Cheung 19e7407f55 Return correct error when violating unique index (#1763) 2016-05-11 17:24:15 -07:00
North 6cfcb4ddf8 Fix #1755 (#1756)
* Add condition at limit = 0

* Add tests for installations with limit and count parameters
2016-05-11 15:22:29 -07:00
Drew 10ee229e60 Update dependencies and support node 6.1 (#1754)
* Update dependencies and support node 6.1

* Remove gcs-adapter, which depends on grpc, which fails to build on node 6

* Remove GCS adapter usage
2016-05-11 08:54:23 -04:00
Sebastián Balay 1291f70a16 Fix malformed config.json in bootstrap.sh
The bootstrap bash script generates a wrong config.json file.
Instead of having a databaseURI property, it has a mongodbURI prop.
2016-05-10 18:13:06 -04:00
Drew 26df50924b Update to 2.2.9 (#1747) 2016-05-10 10:50:05 -07:00
Drew e698f61a58 Handle null param in cloud code. Fixes #1472 (#1746) 2016-05-10 10:43:02 -07:00
Drew e60901de6a Version 2.2.8 (#1738) 2016-05-09 16:56:32 -07:00
Fosco Marotto b2f36e192a Merge pull request #1644 from drew-gross/refactor-query-transform
Break dependency of deleteObjectsByQuery on schemaController
2016-05-09 11:21:22 -07:00
andrewchae 10d2988985 Pass dates to cloud code functions (#655) (#1697) 2016-05-09 10:45:57 -07:00
Steven Shipton 37c502bed0 Add config.expireInactiveSession to add support for non-expiring inactive sessions (#1536)
* Create non-expiring session when sessionLength is zero

* Introduce expireInactiveSessions setting
2016-05-06 12:50:45 -07:00
Héctor Ramos b00572de65 Merge pull request #1696 from ParsePlatform/issue-template
Update issue template
2016-05-06 12:46:41 -07:00
Héctor Ramos da5b5ccefa Update ISSUE_TEMPLATE.md 2016-05-06 12:45:47 -07:00
Steven Shipton 7c850fc1ad Fix for #1642 - copy query parameters to request body (#1687)
* Fix for #1642 - copy query parameters to request body

* Add missing request.query to pass test
2016-05-06 12:36:05 -07:00
Héctor Ramos 90df7841ef Update issue template
Additional guidance for what type of details should be included with a bug report.
2016-05-03 12:08:23 -07:00
Héctor Ramos 0b88302bea Link to new migration guide 2016-04-29 21:34:33 -07:00
Héctor Ramos 583bb34995 Link to bug tool for migration issues 2016-04-28 12:56:04 -07:00
Drew Gross 71ae7bee94 better names and comments 2016-04-26 13:08:58 -07:00
Drew Gross 449ca115b1 Break object deletions's dependency on schemaController 2016-04-26 10:23:14 -07:00
Drew Gross d4bd21fcbc remove schema from transformWhere 2016-04-26 10:20:17 -07:00
Drew Gross a926712951 Start using parse format schema in transformQueryKeyValue 2016-04-26 10:12:45 -07:00
Drew Gross 5f564f32f5 simplify a little 2016-04-25 23:39:42 -07:00
Drew Gross 874d10fc74 Check expected type is array from Parse Format Schema 2016-04-25 23:36:25 -07:00
Drew Gross 4371ca164c Pass parse format schema to transformWhere 2016-04-25 22:45:16 -07:00
Drew Gross 608cba9e8c Clearer names in DatabaseController 2016-04-25 22:12:03 -07:00
Drew Gross 5cbf3eb8dd Tidy up db controller 2016-04-25 22:01:50 -07:00
Drew Gross 37953d146b some cleanup 2016-04-25 21:53:57 -07:00
Drew Gross a3179e43fb No need to handle object or reject arrays when transforming queries 2016-04-25 21:13:34 -07:00
Drew Gross 7b431ad064 nits 2016-04-25 20:54:47 -07:00
Aidan Musnitzky 3d7589c1bc Update README.md (#1637) 2016-04-25 20:51:52 -07:00
Drew Gross ee8a3c94d5 No need to transform post-transform keys in mongo adapter 2016-04-25 20:42:19 -07:00
Drew Gross e2a3213351 Remove update logic from query 2016-04-25 19:34:07 -07:00
Drew Gross 5c7b346292 Remove unnecessary logic 2016-04-25 18:48:22 -07:00
Florent Vilmart 0542f860f4 Test case to ensure dashboard.parse.com won't break (#1636)
* Test case to ensure dashboard.parse.com won't break

* Adds volatile classes concept for _PushStatus

* Fixes test
2016-04-25 21:33:11 -04:00
Drew Gross e0a9f19895 Split logic for transforming queries into it's own function 2016-04-25 18:22:20 -07:00
Drew Gross 21e73ac276 Destructuring 2016-04-25 17:21:24 -07:00
Drew 957b5927b1 Ignore _RevoableSession "header" that is sent by JS SDK. Fixes #1548. (#1627) 2016-04-25 15:52:21 -04:00
Drew 234d0093ff Cleanup delete schema (#1604)
* Some cleanup for deleting one schema

* tidyness

* Remove _allCollections as Parse Server doesn't need it.
2016-04-25 14:47:57 -04:00
Drew d14d451028 Move acl adding into parse server (#1601)
* Move writeACL knowledge out of mongoAdapter

* Remove write ACL from mongo adapter

* Remove readACL from Mongo Transform
2016-04-22 21:44:03 -04:00
Drew d33dd68cc5 Add revokeSessionOnPasswordReset option. Closes #1584 (#1597)
* Add revokeSessionOnPasswordReset option

* Fix nits
2016-04-22 18:21:50 -04:00
Drew 0d094767cf Cleanup update (#1590)
* destructuring in DB controller

* deleteObject in db adapter

* Turns out we can't have delete by object ID because of ACLs...

* Fix tests

* destructure acl

* Don't reject with object
2016-04-22 17:05:21 -04:00
Florent Vilmart ab827e3c2f Forces delete resolution (#1595) 2016-04-22 08:46:27 -07:00
Florent Vilmart d49d539a81 _PushStatus uses proper Parse format (#1580)
* removes mongo style props from _PushStatus

* removes log

* Update MongoTransform.js

Removes _id case
2016-04-21 21:36:15 -04:00
Blayne Chard 6e9529f81c Exposes the loggerAdapter as log to Cloud Functions and Triggers (#1565)
This allows access to logging inside cloud code and triggers via
    request.log.info
    request.log.error
2016-04-21 16:20:14 -04:00
Florent Vilmart 2d94a885d2 regression test for #1554 2016-04-20 21:51:48 -04:00
Florent Vilmart b7b57f7ea7 Adds support for Pointer Permissions
* WIP: Initial pointer permissions

* Process Pointer perms when no matching CLP are found

* Additional tests with read lockdown

* Create operation lockdown with pointer permissions,

on parse.com, when an class is locked down with write pointer perm, users can't create objects even if they set their own as the pointer permission key

* Adds test case for multiple write PointerPerms

* Adds validation for pointer permissions when setting

* Adds tests for validating pointer permissions column types

* Adds tests for complex ACL/CLP/PP hierarchy

* Restores power of the master

* Adds validation of borked fields

* Adds complex test for find

* Adds more variations around PointerPermissions tests
2016-04-20 21:51:11 -04:00
Drew b185508508 Remove flaky test
This test depends on external services and is super flaky. Lets kill it.
2016-04-20 19:03:56 -04:00
Drew 7b91ee98ed Fixes #1568 (#1574) 2016-04-20 18:42:18 -04:00
Drew 9776362ab2 Begin isolating object creation code into an externalizable API. (#1569)
* Tidy up transformKeyValue

* Specialize transformKeyValue for object creation

* remove keys that never appear in creation requests

* rename function

* remove local var

* early exit for simple keys

* Refactor create

* Force class creation when creating an object

* Pass parameters to key value transformer

* No need to check for array in this func

* start using Parse Format schema in MongoTransform

* Remove call to getExpectedType

* add tests to ensure client can't see _PushStatus
2016-04-20 13:35:48 -07:00
Florent Vilmart 59b4047de8 Defers the session creation after DB operation (#1561) 2016-04-20 11:57:38 -04:00
Blayne Chard 54b21c2f73 Compress the ACL queries into a single $in query (#1566) 2016-04-20 09:03:44 -04:00
Drew 5d37755d88 Test facebook unlinking (regression test #1532) (#1538) 2016-04-20 00:31:15 -07:00
Florent Vilmart ddd01181be Safely require babel-polyfill (#1558)
(https://github.com/shakacode/bootstrap-loader/pull/29/files)
2016-04-19 22:29:00 -07:00
Drew bf47a0b2b6 Remove acceptor parameter (#1553) 2016-04-19 14:29:05 -07:00
Florent Vilmart 3945e0c680 Update pushStatusHandler.js (#1543)
Fixes error thrown by #1526
2016-04-18 19:30:19 -07:00
Drew 61b62e4e9f Rename Schema => SchemaController (#1542)
* Rename Schema to SchemaController

* Rename Schema => SchemaController

* Move to controllers folder

* Move SchemasController to Controllers folder

* remove ./..
2016-04-18 18:59:57 -07:00
Drew 0708af17d7 Move "No two geopoints" logic into mongo adapter (#1491)
* Move "No two geopoints" logic into mongo adapter

* Semicolon
2016-04-18 17:10:30 -07:00
Drew cecb2a1cb1 Move 'ns not found' into mongo adapter. (#1541) 2016-04-18 17:06:51 -07:00
Drew ab0ea09067 Schema.js cleanup (#1540)
* Tidy up schemas router

* de-duplicate logic for injecting default schemas

* Remove no-op promise

* use hasClass

* Make getAllSchemas part of SchemaController

* Move getOneSchema logic onto schema controller

* remove log
2016-04-18 17:06:00 -07:00
Drew 414ee6740d Regression test for #1489 (#1511) 2016-04-18 19:45:48 -04:00
Fosco Marotto 65bd59d95f Add link to screencast blog in readme. 2016-04-18 13:08:28 -07:00
Drew ac839f75c7 Remove mongo object create format from Parse Server (#1516) 2016-04-18 12:45:48 -04:00
Drew c57c6b33b1 Add link to mandrill adapter. 2016-04-15 14:33:12 -07:00
Fosco Marotto 4c2636e23c Merge pull request #1510 from ParsePlatform/fosco.227
Changelog and update to 2.2.7
2016-04-15 12:49:26 -07:00
Fosco Marotto 2f51e751de Changelog and update to 2.2.7 2016-04-15 12:46:25 -07:00
Seiji Akiyama a727e1ccd3 Adds limit = 0 as a valid parameter for queries (#1493)
* Remove results if limit = 0;

* Adds tests for limit=0 and count=1.

* Improves readability.
2016-04-15 11:17:53 -07:00
Drew f076078bf1 Add note about URL encoding special characters in passwords. 2016-04-15 10:50:46 -07:00
Drew 3f66c302b7 Enable logs (#1502) 2016-04-14 17:44:50 -07:00
Florent Vilmart eef51cf9ad Hotfix for tests (#1503) 2016-04-14 17:25:02 -07:00
Florent Vilmart 1023baf20d DBController refactoring (#1228)
* Moves transform to MongoTransform

- Adds ACL query injection in MongoTransform

* Removes adaptiveCollection from DatabaseController

- All collections manipulations are now handled by a DBController
- Adds optional flags to configure an unsafe databaseController for direct
  access
- Adds ability to configure RestWrite with multiple writes
- Moves some transfirmations to MongoTransform as they output specific code

* Renames Unsafe to WithoutValidation
2016-04-14 19:24:56 -04:00
Tyler Brock 51970fb470 Do some triple equals for great justice (#1499) 2016-04-14 15:44:22 -07:00
Tyler Brock d57e384dcb Apply credential stripping to all untransforms for _User (#1498) 2016-04-14 14:50:16 -07:00
Florent Vilmart 34851c0ae5 Makes sure we don't delete Installations at large when updating a token (#1475) (#1486) 2016-04-14 12:59:59 -07:00
Simonas Karuzas 686cc4ab95 Checking if object has defined key for Pointer constraints in liveQuery
* Removing sessionToken and authData from _User objects included in a query

This bug caused sessionToken to be replaced on client side to some old
sessionToken from DB.

* Removing dangling variable that is never used

* Checking if object has defined key for Pointer constraints in liveQuery

If there is a liveQuery subscription, with Pointer type constrains (e.g
query.equalTo('user', Parse.User.current())), and new object has
undefined value for that field, we get this error:

    error: Uncaught internal server error. [TypeError: Cannot read
property 'className' of undefined] TypeError: Cannot read property
'className' of undefined
    at matchesKeyConstraints
(…/node_modules/parse-server/lib/LiveQuery/QueryTools.js:145:51)
2016-04-14 10:24:04 -04:00
Drew fc1cdd4408 Remove collection prefix and default mongo URI (#1479)
* Remove collection prefix from DB Controller

* Remove collection prefix from cache

* Revert "Remove collection prefix from cache"

This reverts commit 529d67dd61.

* Remove knowledge of default mongo URI from Parse Server

* Remove adaptive collection paramater from deleteFields

* Tidy up DBAdapter.js
2016-04-13 19:45:07 -04:00
Drew 3fb3ce1ab7 Store collection prefix in mongo adapter, and clean up adapter interface (#1472) 2016-04-13 08:21:53 -04:00
Drew 512251eb58 Move field deletion logic into mongo adapter (#1471) 2016-04-12 22:06:58 -04:00
Florent Vilmart f312f81cbd Adds support for Long and Double mongodb types (fixes #1316) (#1470)
* Adds support for Long and Double mongodb types (fixes #1316)

* Use bigger numbers
2016-04-12 21:19:47 -04:00
Florent Vilmart af30f669cc Push status nits (#1462)
* Sets _id to objectId in _PushStatus (fixes #1457)

* _PushStatus stores serialized payload (fixes #1458)
2016-04-12 17:43:46 -04:00
Florent Vilmart c050a65d49 Schema.js database agnostic (#1468)
* Schema.js database agnostic

* nits
2016-04-12 17:39:27 -04:00
Drew c419106a38 Remove console.log (#1465) 2016-04-12 10:46:33 -07:00
Drew 99ecbb39f5 Move mongo field type logic into mongoadapter (#1432) 2016-04-12 08:06:00 -04:00
Simonas Karuzas e9e561f5e8 Removing sessionToken and authData from _User objects included in a query (#1450)
* Removing sessionToken and authData from _User objects included in a query

This bug caused sessionToken to be replaced on client side to some old
sessionToken from DB.

* Removing dangling variable that is never used
2016-04-12 07:57:31 -04:00
Florent Vilmart 73a3db44ab Fixes #1444 (#1451) 2016-04-11 11:27:55 -07:00
Drew 281568edd2 Fixes #1417
* Fixes #1417

* Cleanup

* Perf improvement

* Hoist constant array

* Improve tests
2016-04-08 19:06:52 -04:00
Florent Vilmart b433fb9b4e Prevents _User lock out when setting ACL on signup or afterwards (#1429) 2016-04-08 13:19:41 -04:00
Steven Shipton 30197a7d84 Expose DatabaseAdapter to simplify application tests (#1121)
* Move helper.clearData to DatabaseAdapter. Expose DatabaseAdapter in index

* fix indentation

* Export DatabaseAdapter in index.js

* Rename clearData to destroyAllDataPermanently. Only export destroyAllDataPermanently from DatabaseAdapter. Update helper

* Expose wrapped TestUtils from index.js. TestUtils exposed select functions from other modules, only in test environment
2016-04-08 10:00:16 -07:00
Florent Vilmart ab6925a5e2 Update .travis.yml (#1428)
Adds regular expression to match tags (yes, match tag in branches...)
Tags should follow:

1.0.0
or
1.0.0-some
2016-04-08 08:02:54 -04:00
Florent Vilmart a770c4cba8 Adds relation fields to objects 2016-04-08 07:42:02 -04:00
Florent Vilmart de2a2ee245 Update .travis.yml (#1423)
* Update .travis.yml

This will prevent the additional build we're seeing, as well as hopefully keep the tagged deploys
Only run the deployment when $MONGODB_VERSION = '3.0.8' (one part of the build matrix)
2016-04-07 23:11:06 -04:00
Florent Vilmart c22bafb951 Sets the defaultSchemas keys in the SchemaCollection (#1421)
* Sets the defaultSchemas keys in the SchemaCollection

* Moves defaultSchema injection logic to router

* maps outside the function

* fixes test

* conciseness
2016-04-07 22:13:07 -04:00
Florent Vilmart bc96f0beb1 Adds X-Parse-Push-Status-Id header (#1412)
* Adds X-Parse-Push-Status-Id header

* Waits for _PushStatus to be stored
2016-04-07 18:08:09 -04:00
Florent Vilmart cedac3fda6 Adds support for --verbose and verbose option when running ParseServer (#1414) 2016-04-07 18:07:52 -04:00
Blayne Chard 616ac71623 Untransform should treat Array's as nested objects (#1416) 2016-04-07 14:43:48 -07:00
Drew 325d09ca4c Schema format cleanup 2016-04-07 07:47:47 -04:00
Florent Vilmart ef92a79818 Updates the publicServerURL option [ci skip] 2016-04-07 07:45:22 -04:00
Drew 91ace4e718 Progres towards moving mongo logic into its adapter 2016-04-05 21:16:39 -07:00
Florent Vilmart cbbd66964a Huge performance improvement on roles queries 2016-04-05 20:44:26 -04:00
0xA455 1bd804693c Fix exception with non-expiring session tokens.
Session tokens generated by Parse with with "Expire inactive session" set

to No leave the the expiresAt field undefined. This fixes the TypeError

"Cannot read property 'iso' of undefined'" exception thrown when trying

to use a session token with undefined expiresAt.
2016-04-05 16:12:15 -07:00
Florent Vilmart 1eb2a87f9f Removes GCS Adapter from provided adapters
* 💣 removes exposed GCS files adapter

- Should fix problems with GCS
- Throws error that shows the right repo
2016-04-05 18:18:49 -04:00
Fosco Marotto 0b2dc12143 Merge pull request #1226 from 1nput0utput/master
Spotify authentication
2016-04-05 10:49:18 -07:00
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
André Luiz Cardoso 3dccd61222 Matching queries with doesNotExist constraint 2016-03-29 11:28:45 -03:00
Rajinder Ramgarhia cbf3f3c1be Spotify authentication 2016-03-28 02:07:10 -04:00
252 changed files with 29431 additions and 10122 deletions
+21
View File
@@ -0,0 +1,21 @@
node_modules
npm-debug.log
*.md
PATENTS
LICENSE
Dockerfile
.dockerignore
.gitignore
.travis.yml
.istanbul.yml
.git
.github
# Build folder
lib/
# Tests
spec/
# IDEs
.idea/
+2
View File
@@ -0,0 +1,2 @@
lib/*
coverage/*
+26
View File
@@ -0,0 +1,26 @@
{
"root": true,
"extends": "eslint:recommended",
"env": {
"node": true,
"es6": true
},
"parser": "babel-eslint",
"plugins": [
"flowtype"
],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"rules": {
"indent": ["error", 2],
"linebreak-style": ["error", "unix"],
"no-trailing-spaces": 2,
"eol-last": 2,
"space-in-parens": ["error", "never"],
"no-multiple-empty-lines": 1,
"prefer-const": "error",
"space-infix-ops": "error"
}
}
+34 -8
View File
@@ -1,17 +1,43 @@
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.
We use GitHub Issues for bugs.
Make sure these boxes are checked before submitting your issue -- thanks for reporting issues back to Parse Server!
If you have a non-bug question, ask on Stack Overflow or Server Fault:
- https://stackoverflow.com/questions/tagged/parse.com
- https://serverfault.com/tags/parse
- [ ] You've met the [prerequisites](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites).
You may also search through existing issues before opening a new one: https://github.com/ParsePlatform/Parse-Server/issues?utf8=%E2%9C%93&q=is%3Aissue
- [ ] You're running the [latest version](https://github.com/ParsePlatform/parse-server/releases) of Parse Server.
--- Please use this template. If you don't use this template, your issue may be closed without comment. ---
- [ ] 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.
### Issue Description
#### Environment Setup
Describe your issue in as much detail as possible.
### Steps to reproduce
#### Steps to reproduce
Please include a detailed list of steps that reproduce the issue. Include curl commands when applicable.
#### Expected Results
#### Logs/Trace
What you expected to happen.
#### Actual Outcome
What is happening instead.
### Environment Setup
- **Server**
- parse-server version (Be specific! Don't say 'latest'.) : [FILL THIS OUT]
- Operating System: [FILL THIS OUT]
- Hardware: [FILL THIS OUT]
- Localhost or remote server? (AWS, Heroku, Azure, Digital Ocean, etc): [FILL THIS OUT]
- **Database**
- MongoDB version: [FILL THIS OUT]
- Storage engine: [FILL THIS OUT]
- Hardware: [FILL THIS OUT]
- Localhost or remote server? (AWS, mLab, ObjectRocket, Digital Ocean, etc): [FILL THIS OUT]
### Logs/Trace
Include all relevant logs. You can turn on additional logging by configuring VERBOSE=1 in your environment.
+4
View File
@@ -23,6 +23,9 @@ coverage
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# build folder for automated releases
latest
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
@@ -41,6 +44,7 @@ lib/
# cache folder
.cache
.eslintcache
# Mac DS_Store files
.DS_Store
+2
View File
@@ -0,0 +1,2 @@
instrumentation:
excludes: ["**/spec/**"]
+65 -12
View File
@@ -1,16 +1,69 @@
language: node_js
dist: trusty
node_js:
- '4.6'
- '6.1'
services:
- postgresql
- redis-server
- docker
addons:
postgresql: '9.5'
apt_packages:
- postgresql-9.5-postgis-2.3
before_script:
- node -e 'require("./lib/index.js")'
- psql -c 'create database parse_server_postgres_adapter_test_database;' -U postgres
- psql -c 'CREATE EXTENSION postgis;' -U postgres -d parse_server_postgres_adapter_test_database
- psql -c 'CREATE EXTENSION postgis_topology;' -U postgres -d parse_server_postgres_adapter_test_database
env:
global:
- COVERAGE_OPTION='./node_modules/.bin/istanbul cover'
matrix:
- MONGODB_VERSION=3.0.8
- MONGODB_VERSION=3.2.6
- PARSE_SERVER_TEST_DB=postgres
- PARSE_SERVER_TEST_CACHE=redis
matrix:
fast_finish: true
branches:
only:
- master
language: node_js
node_js:
- "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
- master
- /^[0-9]+.[0-9]+.[0-9]+(-.*)?$/
cache:
directories:
- $HOME/.mongodb/versions/downloads
after_success: ./node_modules/.bin/codecov
- node_modules
- "$HOME/.mongodb/versions"
after_script:
- bash <(curl -s https://codecov.io/bash)
after_failure:
- (cd resources; python travis_after_all.py)
after_success:
- (cd resources; python travis_after_all.py)
- export $(cat resources/.to_export_back)
deploy:
- provider: npm
skip_cleanup: true
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
all_branches: true
condition: "$BUILD_LEADER$BUILD_AGGREGATE_STATUS = YESothers_succeeded"
repo: ParsePlatform/parse-server
- provider: script
skip_cleanup: true
script: ./resources/npm-git.sh
on:
branch: master
condition: "$BUILD_LEADER$BUILD_AGGREGATE_STATUS = YESothers_succeeded"
- provider: script
skip_cleanup: true
script: ./resources/docker-hub.sh
on:
tags: true
all_branches: true
condition: "$BUILD_LEADER$BUILD_AGGREGATE_STATUS = YESothers_succeeded"
+82
View File
@@ -0,0 +1,82 @@
# Upgrading Parse Server to version 2.3.0
Parse Server version 2.3.0 begins using unique indexes to ensure the User's username and email are unique. This is not a backwards incompatible change, but it may in some cases cause a significant performance regression until the index finishes building. Building the unique index before upgrading your Parse Server version will eliminate the performance impact, and is a recommended step before upgrading any app to Parse Server 2.3.0. New apps starting with version 2.3.0 do not need to take any steps before beginning their project.
If you are using MongoDB in Cluster or Replica Set mode, we recommend reading Mongo's [documentation on index building](https://docs.mongodb.com/v3.0/tutorial/build-indexes-on-replica-sets/) first. If you are not using these features, you can execute the following commands from the Mongo shell to build the unique index. You may also want to create a backup first.
```js
// Select the database that your Parse App uses
use parse;
// Select the collection your Parse App uses for users. For migrated apps, this probably includes a collectionPrefix.
var coll = db['your_prefix:_User'];
// You can check if the indexes already exists by running coll.getIndexes()
coll.getIndexes();
// The indexes you want should look like this. If they already exist, you can skip creating them.
{
"v" : 1,
"unique" : true,
"key" : {
"username" : 1
},
"name" : "username_1",
"ns" : "parse.your_prefix:_User",
"background" : true,
"sparse" : true
}
{
"v" : 1,
"unique" : true,
"key" : {
"email" : 1
},
"name" : "email_1",
"ns" : "parse.your_prefix:_User",
"background" : true,
"sparse" : true
}
// Create the username index.
// "background: true" is mandatory and avoids downtime while the index builds.
// "sparse: true" is also mandatory because Parse Server uses sparse indexes.
coll.ensureIndex({ username: 1 }, { background: true, unique: true, sparse: true });
// Create the email index.
// "background: true" is still mandatory.
// "sparse: true" is also mandatory both because Parse Server uses sparse indexes, and because email addresses are not required by the Parse API.
coll.ensureIndex({ email: 1 }, { background: true, unique: true, sparse: true });
```
There are some issues you may run into during this process:
## Mongo complains that the index already exists, but with different options
In this case, you will need to remove the incorrect index. If your app relies on the existence of the index in order to be performant, you can create a new index, with "-1" for the direction of the field, so that it counts as different options. Then, drop the conflicting index, and create the unique index.
## There is already non-unique data in the username or email field
This is possible if you have explicitly set some user's emails to null. If this is bogus data, and those null fields should be unset, you can unset the null emails with this command. If your app relies on the difference between null and unset emails, you will need to upgrade your app to treat null and unset emails the same before building the index and upgrading to Parse Server 2.3.0.
```js
coll.update({ email: { $exists: true, $eq: null } }, { $unset: { email: '' } }, { multi: true })
```
## There is already non-unique data in the username or email field, and it's not nulls
This is possible due to a race condition in previous versions of Parse Server. If you have this problem, it is unlikely that you have a lot of rows with duplicate data. We recommend you clean up the data manually, by removing or modifying the offending rows.
This command, can be used to find the duplicate data:
```js
coll.aggregate([
{$match: {"username": {"$ne": null}}},
{$group: {_id: "$username", uniqueIds: {$addToSet: "$_id"}, count: {$sum: 1}}},
{$match: {count: {"$gt": 1}}},
{$project: {id: "$uniqueIds", username: "$_id", _id : 0} },
{$unwind: "$id" },
{$out: '_duplicates'} // Save the list of duplicates to a new, "_duplicates" collection. Remove this line to just output the list.
], {allowDiskUse:true})
```
+455 -3
View File
@@ -1,5 +1,459 @@
## Parse Server Changelog
### 2.3.7
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.3.6...2.3.7)
#### New Features
* New endpoint to resend verification email, thanks to [Xy Ziemba](https://github.com/xyziemba)
#### Improvements
* Add TTL option for Redis Cache Adapter, thanks to [Ryan Foster](https://github.com/f0ster)
* Update Postgres Storage Adapter, thanks to [Vitaly Tomilov](https://github.com/vitaly-t)
#### Bug Fixes
* Add index on Role.name, fixes (#3579), thanks to [Natan Rolnik](https://github.com/natanrolnik)
* Fix default value of userSensitiveFields, fixes (#3593), thanks to [Arthur Cinader](https://github.com/acinader)
#### Dependency Updates:
* [body-parser@1.17.1](https://www.npmjs.com/package/body-parser)
* [express@4.15.2](https://www.npmjs.com/package/express)
* [request@2.81.0](https://www.npmjs.com/package/request)
* [winston-daily-rotate-file@1.4.5](https://www.npmjs.com/package/winston-daily-rotate-file)
* [ws@2.2.0](https://www.npmjs.com/package/ws)
### 2.3.6
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.3.5...2.3.6)
#### Improvements
* Adds support for injecting a middleware for instumentation in the CLI, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Alleviate mongodb bug with $or queries [SERVER-13732](https://jira.mongodb.org/browse/SERVER-13732), thanks to [Jack Wearden](https://github.com/NotBobTheBuilder)
#### Bug Fixes
* Fix issue affecting password policy and empty passwords, thanks to [Bhaskar Reddy Yasa](https://github.com/bhaskaryasa)
* Fix issue when logging url in non string objects, thanks to [Paulo Vítor S Reis](https://github.com/paulovitin)
#### Dependencies updates:
* [ws@2.1.0](https://npmjs.com/package/ws)
* [uws@0.13.0](https://npmjs.com/package/uws)
* [pg-promise@5.6.2](https://npmjs.com/package/pg-promise)
### 2.3.5
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.3.3...2.3.5)
#### Bug Fixes
* Allow empty client key
(#3497), thanks to [Arthur Cinader](https://github.com/acinader)
* Fix LiveQuery unsafe user
(#3525), thanks to [David Starke](https://github.com/dstarke)
* Use `flushdb` instead of `flushall` in RedisCacheAdapter
(#3523), thanks to [Jeremy Louie](https://github.com/JeremyPlease)
* Fix saving GeoPoints and Files in `_GlobalConfig` (Make sure we don't treat
dot notation keys as topLevel atoms)
(#3531), thanks to [Florent Vilmart](https://github.com/flovilmart)
### 2.3.3
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.3.2...2.3.3)
#### Breaking Changes
* **Minimum Node engine bumped to 4.6** (#3480), thanks to [Florent Vilmart](https://github.com/flovilmart)
#### Bug Fixes
* Add logging on failure to create file (#3424), thanks to [Arthur Cinader](https://github.com/acinader)
* Log Parse Errors so they are intelligible (#3431), thanks to [Arthur Cinader](https://github.com/acinader)
* MongoDB $or Queries avoid SERVER-13732 bug (#3476), thanks to [Jack Wearden](https://github.com/NotBobTheBuilder)
* Mongo object to Parse object date serialization - avoid re-serialization of iso of type Date (#3389), thanks to [nodechefMatt](https://github.com/nodechefMatt)
#### Improvements
* Ground preparations for push scalability (#3080), thanks to [Florent Vilmart](https://github.com/flovilmart)
* Use uWS as optional dependency for ws server (#3231), thanks to [Florent Vilmart](https://github.com/flovilmart)
### 2.3.2
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.3.1...2.3.2)
#### New features
* Add parseFrameURL for masking user-facing pages (#3267), thanks to [Lenart Rudel](https://github.com/lenart)
#### Bug fixes
* Fix Parse-Server to work with winston-daily-rotate-1.4.2 (#3335), thanks to [Arthur Cinader](https://github.com/acinader)
#### Improvements
* Add support for regex string for password policy validatorPattern setting (#3331), thanks to [Bhaskar Reddy Yasa](https://github.com/bhaskaryasa)
* LiveQuery should match subobjects with dot notation (#3322), thanks to [David Starke](https://github.com/dstarke)
* Reduce time to process high number of installations for push (#3264), thanks to [jeacott1](https://github.com/jeacott1)
* Fix trivial typo in error message (#3238), thanks to [Arthur Cinader](https://github.com/acinader)
### 2.3.1
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.3.0...2.3.1)
A major issue was introduced when refactoring the authentication modules.
This release addresses only that issue.
### 2.3.0
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.2.25...2.3.0)
#### Breaking changes
* Parse.Cloud.useMasterKey() is a no-op, please refer to (Cloud Code migration guide)[https://github.com/ParsePlatform/parse-server/wiki/Compatibility-with-Hosted-Parse#cloud-code]
* Authentication helpers are now proper adapters, deprecates oauth option in favor of auth.
* DEPRECATES: facebookAppIds, use `auth: { facebook: { appIds: ["AAAAAAAAA" ] } }`
* `email` field is not returned anymore for `Parse.User` queries. (Provided only on the user itself if provided).
#### New Features
* Adds ability to restrict access through Class Level Permissions to only authenticated users [see docs](http://parseplatform.github.io/docs/ios/guide/#requires-authentication-permission-requires-parse-server---230)
* Adds ability to strip sensitive data from `_User` responses, strips emails by default, thanks to [Arthur Cinader](https://github.com/acinader)
* Adds password history support for password policies, thanks to [Bhaskar Reddy Yasa](https://github.com/bhaskaryasa)
#### Improvements
* Bump parse-server-s3-adapter to 1.0.6, thanks to [Arthur Cinader](https://github.com/acinader)
* Using PARSE_SERVER_ENABLE_EXPERIMENTAL_DIRECT_ACCESS let you create user sessions when passing {installationId: "xxx-xxx"} on signup in cloud code, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Add CLI option to pass `host` parameter when creating parse-server from CLI, thanks to [Kulshekhar Kabra](https://github.com/kulshekhar)
#### Bug fixes
* Ensure batch routes are only using posix paths, thanks to [Steven Shipton](https://github.com/steven-supersolid)
* Ensure falsy options from CLI are properly taken into account, thanks to [Steven Shipton](https://github.com/steven-supersolid)
* Fixes issues affecting calls to `matchesKeyInQuery` with pointers.
* Ensure that `select` keys can be changed in triggers (beforeFind...), thanks to [Arthur Cinader](https://github.com/acinader)
#### Housekeeping
* Enables and enforces linting with eslint, thanks to [Arthur Cinader](https://github.com/acinader)
### 2.2.25
Postgres support requires v9.5
#### New Features
* Dockerizing Parse Server, thanks to [Kirill Kravinsky](https://github.com/woyorus)
* Login with qq, wechat, weibo, thanks to [haifeizhang]()
* Password policy, validation and expiration, thanks to [Bhaskar Reddy Yasa](https://github.com/bhaskaryasa)
* Health check on /health, thanks to [Kirill Kravinsky](https://github.com/woyorus)
* Reuse SchemaCache across requests option, thanks to [Steven Shipton](https://github.com/steven-supersolid)
#### Improvements
* Better support for CLI options, thanks to [Steven Shipton](https://github.com/steven-supersolid)
* Specity a database timeout with maxTimeMS, thanks to [Tyler Brock](https://github.com/TylerBrock)
* Adds the username to reset password success pages, thanks to [Halim Qarroum](https://github.com/HQarroum)
* Better support for Redis cache adapter, thanks to [Tyler Brock](https://github.com/TylerBrock)
* Better coverage of Postgres, thanks to [Kulshekhar Kabra](https://github.com/kulshekhar)
#### Bug Fixes
* Fixes issue when sending push to multiple installations, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fixes issues with twitter authentication, thanks to [jonas-db](https://github.com/jonas-db)
* Ignore createdAt fields update, thanks to [Yuki Takeichi](https://github.com/yuki-takeichi)
* Improve support for array equality with LiveQuery, thanks to [David Poetzsch-Heffter](https://github.com/dpoetzsch)
* Improve support for batch endpoint when serverURL and publicServerURL have different paths, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Support saving relation objects, thanks to [Yuki Takeichi](https://github.com/yuki-takeichi)
### 2.2.24
#### New Features
* LiveQuery: Bring your own adapter (#2902), thanks to [Florent Vilmart](https://github.com/flovilmart)
* LiveQuery: Adds "update" operator to update a query subscription (#2935), thanks to [Florent Vilmart](https://github.com/flovilmart)
#### Improvements
* Better Postgres support, thanks to [Kulshekhar Kabra](https://github.com/kulshekhar)
* Logs the function name when failing (#2963), thanks to [Michael Helvey](https://github.com/michaelhelvey)
* CLI: forces closing the connections with SIGINT/SIGTERM (#2964), thanks to [Kulshekhar Kabra](https://github.com/kulshekhar)
* Reduce the number of calls to the `_SCHEMA` table (#2912), thanks to [Steven Shipton](https://github.com/steven-supersolid)
* LiveQuery: Support for Role ACL's, thanks to [Aaron Blondeau](https://github.com/aaron-blondeau-dose)
#### Bug Fixes
* Better support for checking application and client keys, thanks to [Steven Shipton](https://github.com/steven-supersolid)
* Google OAuth, better support for android and web logins, thanks to [Florent Vilmart](https://github.com/flovilmart)
### 2.2.23
* Run liveQuery server from CLI with a different port, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Support for Postgres databaseURI, thanks to [Kulshekhar Kabra](https://github.com/kulshekhar)
* Support for Postgres options, thanks to [Kulshekhar Kabra](https://github.com/kulshekhar)
* Improved support for google login (id_token and access_token), thanks to [Florent Vilmart](https://github.com/flovilmart)
* Improvements with VKontakte login, thanks to [Eugene Antropov](https://github.com/antigp)
* Improved support for `select` and `include`, thanks to [Florent Vilmart](https://github.com/flovilmart)
#### Bug fixes
* Fix error when updating installation with useMasterKey (#2888), thanks to [Jeremy Louie](https://github.com/JeremyPlease)
* Fix bug affecting usage of multiple `notEqualTo`, thanks to [Jeremy Louie](https://github.com/JeremyPlease)
* Improved support for null values in arrays, thanks to [Florent Vilmart](https://github.com/flovilmart)
### 2.2.22
* Minimum nodejs engine is now 4.5
#### New Features
* New: CLI for parse-live-query-server, thanks to [Florent Vilmart](https://github.com/flovilmart)
* New: Start parse-live-query-server for parse-server CLI, thanks to [Florent Vilmart](https://github.com/flovilmart)
#### Bug fixes
* Fix: Include with pointers are not conflicting with get CLP anymore, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Removes dependency on babel-polyfill, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Support nested select calls, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Use native column selection instead of runtime, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: installationId header is properly used when updating `_Installation` objects, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: don't crash parse-server on improperly formatted live-query messages, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Passwords are properly stripped out of logs, thanks to [Arthur Cinader](https://github.com/acinader)
* Fix: Lookup for email in username if email is not set, thanks to [Florent Vilmart](https://github.com/flovilmart)
### 2.2.21
* Fix: Reverts removal of babel-polyfill
### 2.2.20
* New: Adds CloudCode handler for `beforeFind`, thanks to [Florent Vilmart](https://github.com/flovilmart)
* New: RedisCacheAdapter for syncing schema, role and user caches across servers, thanks to [Florent Vilmart](https://github.com/flovilmart)
* New: Latest master build available at `ParsePlatform/parse-server#latest`, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Better support for upgradeToRevocableSession with missing session token, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Removes babel-polyfill runtime dependency, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Cluster option now support a boolean value for automatically choosing the right number of processes, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Filenames now appear correctly, thanks to [Lama Chandrasena](https://github.com/lama-buddy)
* Fix: `_acl` is properly updated, thanks to [Steven Shipton](https://github.com/steven-supersolid)
Other fixes by [Mathias Rangel Wulff](https://github.com/mathiasrw)
### 2.2.19
* New: support for upgrading to revocable sessions, thanks to [Florent Vilmart](https://github.com/flovilmart)
* New: NullCacheAdapter for disabling caching, thanks to [Yuki Takeichi](https://github.com/yuki-takeichi)
* New: Account lockout policy [#2601](https://github.com/ParsePlatform/parse-server/pull/2601), thanks to [Diwakar Cherukumilli](https://github.com/cherukumilli)
* New: Jobs endpoint for defining and run jobs (no scheduling), thanks to [Florent Vilmart](https://github.com/flovilmart)
* New: Add --cluster option to the CLI, thanks to [Florent Vilmart](https://github.com/flovilmart)
* New: Support for login with vk.com, thanks to [Nurdaulet Bolatov](https://github.com/nbolatov)
* New: experimental support for postgres databases, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: parse-server doesn't call next() after successful responses, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Nested objects are properly includeed with Pointer Permissions on, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: null values in include calls are properly handled, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Schema validations now runs after beforeSave hooks, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: usersname and passwords are properly type checked, thanks to [Bam Wang](https://github.com/bamwang)
* Fix: logging in info log would log also in error log, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: removes extaneous logging from ParseLiveQueryServer, thanks to [Flavio Torres](https://github.com/flavionegrao)
* Fix: support for Range requests for files, thanks to [Brage G. Staven](https://github.com/Bragegs)
### 2.2.18
* Fix: Improve support for objects in push alert, thanks to [Antoine Lenoir](https://github.com/alenoir)
* Fix; Prevent pointed from getting clobbered when they are changed in a beforeSave, thanks to [sud](https://github.com/sud80)
* Fix: Improve support for "Bytes" type, thanks to [CongHoang](https://github.com/conghoang)
* Fix: Better logging compatability with Parse.com, thanks to [Arthur Cinader](https://github.com/acinader)
* New: Add Janrain Capture and Janrain Engage auth provider, thanks to [Andrew Lane](https://github.com/AndrewLane)
* Improved: Include content length header in files response, thanks to [Steven Van Bael](https://github.com/vbsteven)
* Improved: Support byte range header for files, thanks to [Brage G. Staven](https://github.com/Bragegs)
* Improved: Validations for LinkedIn access_tokens, thanks to [Felix Dumit](https://github.com/felix-dumit)
* Improved: Experimental postgres support, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Perf: Use native bcrypt implementation if available, thanks to [Florent Vilmart](https://github.com/flovilmart)
### [2.2.17](https://github.com/ParsePlatform/parse-server/tree/2.2.17) (07/23/2016)
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.2.16...2.2.17)
* Cloud code logs [\#2370](https://github.com/ParsePlatform/parse-server/pull/2370) ([flovilmart](https://github.com/flovilmart))
* Make sure \_PushStatus operations are run in order [\#2367](https://github.com/ParsePlatform/parse-server/pull/2367) ([flovilmart](https://github.com/flovilmart))
* Typo fix for error message when can't ensure uniqueness of user email addresses [\#2360](https://github.com/ParsePlatform/parse-server/pull/2360) ([AndrewLane](https://github.com/AndrewLane))
* LiveQuery constrains matching fix [\#2357](https://github.com/ParsePlatform/parse-server/pull/2357) ([simonas-notcat](https://github.com/simonas-notcat))
* Fix typo in logging for commander parseConfigFile [\#2352](https://github.com/ParsePlatform/parse-server/pull/2352) ([AndrewLane](https://github.com/AndrewLane))
* Fix minor typos in test names [\#2351](https://github.com/ParsePlatform/parse-server/pull/2351) ([acinader](https://github.com/acinader))
* Makes sure we don't strip authData or session token from users using masterKey [\#2348](https://github.com/ParsePlatform/parse-server/pull/2348) ([flovilmart](https://github.com/flovilmart))
* Run coverage with istanbul [\#2340](https://github.com/ParsePlatform/parse-server/pull/2340) ([flovilmart](https://github.com/flovilmart))
* Run next\(\) after successfully sending data to the client [\#2338](https://github.com/ParsePlatform/parse-server/pull/2338) ([blacha](https://github.com/blacha))
* Cache all the mongodb/version folder [\#2336](https://github.com/ParsePlatform/parse-server/pull/2336) ([flovilmart](https://github.com/flovilmart))
* updates usage of setting: emailVerifyTokenValidityDuration [\#2331](https://github.com/ParsePlatform/parse-server/pull/2331) ([cherukumilli](https://github.com/cherukumilli))
* Update Mongodb client to 2.2.4 [\#2329](https://github.com/ParsePlatform/parse-server/pull/2329) ([flovilmart](https://github.com/flovilmart))
* Allow usage of analytics adapter [\#2327](https://github.com/ParsePlatform/parse-server/pull/2327) ([deashay](https://github.com/deashay))
* Fix flaky tests [\#2324](https://github.com/ParsePlatform/parse-server/pull/2324) ([flovilmart](https://github.com/flovilmart))
* don't serve null authData values [\#2320](https://github.com/ParsePlatform/parse-server/pull/2320) ([yuzeh](https://github.com/yuzeh))
* Fix null relation problem [\#2319](https://github.com/ParsePlatform/parse-server/pull/2319) ([flovilmart](https://github.com/flovilmart))
* Clear the connectionPromise upon close or error [\#2314](https://github.com/ParsePlatform/parse-server/pull/2314) ([flovilmart](https://github.com/flovilmart))
* Report validation errors with correct error code [\#2299](https://github.com/ParsePlatform/parse-server/pull/2299) ([flovilmart](https://github.com/flovilmart))
* Parses correctly Parse.Files and Dates when sent to Cloud Code Functions [\#2297](https://github.com/ParsePlatform/parse-server/pull/2297) ([flovilmart](https://github.com/flovilmart))
* Adding proper generic Not Implemented. [\#2292](https://github.com/ParsePlatform/parse-server/pull/2292) ([vitaly-t](https://github.com/vitaly-t))
* Adds schema caching capabilities \(5s by default\) [\#2286](https://github.com/ParsePlatform/parse-server/pull/2286) ([flovilmart](https://github.com/flovilmart))
* add digits oauth provider [\#2284](https://github.com/ParsePlatform/parse-server/pull/2284) ([ranhsd](https://github.com/ranhsd))
* Improve installations query [\#2281](https://github.com/ParsePlatform/parse-server/pull/2281) ([flovilmart](https://github.com/flovilmart))
* Adding request headers to cloud functions fixes \#1461 [\#2274](https://github.com/ParsePlatform/parse-server/pull/2274) ([blacha](https://github.com/blacha))
* Creates a new sessionToken when updating password [\#2266](https://github.com/ParsePlatform/parse-server/pull/2266) ([flovilmart](https://github.com/flovilmart))
* Add Gitter chat link to the README. [\#2264](https://github.com/ParsePlatform/parse-server/pull/2264) ([nlutsenko](https://github.com/nlutsenko))
* Restores ability to include non pointer keys [\#2263](https://github.com/ParsePlatform/parse-server/pull/2263) ([flovilmart](https://github.com/flovilmart))
* Allow next middleware handle error in handleParseErrors [\#2260](https://github.com/ParsePlatform/parse-server/pull/2260) ([mejcz](https://github.com/mejcz))
* Exposes the ClientSDK infos if available [\#2259](https://github.com/ParsePlatform/parse-server/pull/2259) ([flovilmart](https://github.com/flovilmart))
* Adds support for multiple twitter auths options [\#2256](https://github.com/ParsePlatform/parse-server/pull/2256) ([flovilmart](https://github.com/flovilmart))
* validate\_purchase fix for SANDBOX requests [\#2253](https://github.com/ParsePlatform/parse-server/pull/2253) ([valeryvaskabovich](https://github.com/valeryvaskabovich))
### 2.2.16 (7/10/2016)
* New: Expose InMemoryCacheAdapter publicly, thanks to [Steven Shipton](https://github.com/steven-supersolid)
* New: Add ability to prevent login with unverified email, thanks to [Diwakar Cherukumilli](https://github.com/cherukumilli)
* Improved: Better error message for incorrect type, thanks to [Andrew Lane](https://github.com/AndrewLane)
* Improved: Better error message for permission denied, thanks to [Blayne Chard](https://github.com/blacha)
* Improved: Update authData on login, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Improved: Ability to not check for old files on Parse.com, thanks to [OzgeAkin](https://github.com/OzgeAkin)
* Fix: Issues with email adapter validation, thanks to [Tyler Brock](https://github.com/TylerBrock)
* Fix: Issues with nested $or queries, thanks to [Florent Vilmart](https://github.com/flovilmart)
### 2.2.15 (6/30/2016)
* Fix: Type in description for Parse.Error.INVALID_QUERY, thanks to [Andrew Lane](https://github.com/AndrewLane)
* Improvement: Stop requiring verifyUserEmails for password reset functionality, thanks to [Tyler Brock](https://github.com/TylerBrock)
* Improvement: Kill without validation, thanks to [Drew Gross](https://github.com/drew-gross)
* Fix: Deleting a file does not delete from fs.files, thanks to [David Keita](https://github.com/maninga)
* Fix: Postgres stoage adapter fix, thanks to [Vitaly Tomilov](https://github.com/vitaly-t)
* Fix: Results invalid session when providing an invalid session token, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: issue creating an anonymous user, thanks to [Hussam Moqhim](https://github.com/hmoqhim)
* Fix: make http response serializable, thanks to [Florent Vilmart](https://github.com/flovilmart)
* New: Add postmark email adapter alternative [Glenn Reyes](https://github.com/glennreyes)
### 2.2.14 (6/25/2016)
* Hotfix: Fix Parse.Cloud.HTTPResponse serialization
### 2.2.13 (6/12/2016)
* Hotfix: Pin version of deepcopy
### 2.2.12 (6/9/2016)
* New: Custom error codes in cloud code response.error, thanks to [Jeremy Pease](https://github.com/JeremyPlease)
* Fix: Crash in beforeSave when response is not an object, thanks to [Tyler Brock](https://github.com/TylerBrock)
* Fix: Allow "get" on installations
* Fix: Fix overly restrictive Class Level Permissions, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Fix nested date parsing in Cloud Code, thanks to [Marco Cheung](https://github.com/Marco129)
* Fix: Support very old file formats from Parse.com
### 2.2.11 (5/31/2016)
* Security: Censor user password in logs, thanks to [Marco Cheung](https://github.com/Marco129)
* New: Add PARSE_SERVER_LOGS_FOLDER env var for setting log folder, thanks to [KartikeyaRokde](https://github.com/KartikeyaRokde)
* New: Webhook key support, thanks to [Tyler Brock](https://github.com/TylerBrock)
* Perf: Add cache adapter and default caching of certain objects, thanks to [Blayne Chard](https://github.com/blacha)
* Improvement: Better error messages for schema type mismatches, thanks to [Jeremy Pease](https://github.com/JeremyPlease)
* Improvement: Better error messages for reset password emails
* Improvement: Webhook key support in CLI, thanks to [Tyler Brock](https://github.com/TylerBrock)
* Fix: Remove read only fields when using beforeSave, thanks to [Tyler Brock](https://github.com/TylerBrock)
* Fix: Use content type provided by JS SDK, thanks to [Blayne Chard](https://github.com/blacha) and [Florent Vilmart](https://github.com/flovilmart)
* Fix: Tell the dashboard the stored push data is available, thanks to [Jeremy Pease](https://github.com/JeremyPlease)
* Fix: Add support for HTTP Basic Auth, thanks to [Hussam Moqhim](https://github.com/hmoqhim)
* Fix: Support for MongoDB version 3.2.6, (note: do not use MongoDB 3.2 with migrated apps that still have traffic on Parse.com), thanks to [Tyler Brock](https://github.com/TylerBrock)
* Fix: Prevent `pm2` from crashing when push notifications fail, thanks to [benishak](https://github.com/benishak)
* Fix: Add full list of default _Installation fields, thanks to [Jeremy Pease](https://github.com/JeremyPlease)
* Fix: Strip objectId out of hooks responses, thanks to [Tyler Brock](https://github.com/TylerBrock)
* Fix: Fix external webhook response format, thanks to [Tyler Brock](https://github.com/TylerBrock)
* Fix: Fix beforeSave when object is passed to `success`, thanks to [Madhav Bhagat](https://github.com/codebreach)
* Fix: Remove use of deprecated APIs, thanks to [Emad Ehsan](https://github.com/emadehsan)
* Fix: Crash when multiple Parse Servers on the same machine try to write to the same logs folder, thanks to [Steven Shipton](https://github.com/steven-supersolid)
* Fix: Various issues with key names in `Parse.Object`s
* Fix: Treat Bytes type properly
* Fix: Caching bugs that caused writes by masterKey or other session token to not show up to users reading with a different session token
* Fix: Pin mongo driver version, preventing a regression in version 2.1.19
* Fix: Various issues with pointer fields not being treated properly
* Fix: Issues with pointed getting un-fetched due to changes in beforeSave
* Fix: Fixed crash when deleting classes that have CLPs
### 2.2.10 (5/15/2016)
* Fix: Write legacy ACLs to Mongo so that clients that still go through Parse.com can read them, thanks to [Tyler Brock](https://github.com/TylerBrock) and [carmenlau](https://github.com/carmenlau)
* Fix: Querying installations with limit = 0 and count = 1 now works, thanks to [ssk7833](https://github.com/ssk7833)
* Fix: Return correct error when violating unique index, thanks to [Marco Cheung](https://github.com/Marco129)
* Fix: Allow unsetting user's email, thanks to [Marco Cheung](https://github.com/Marco129)
* New: Support for Node 6.1
### 2.2.9 (5/9/2016)
* Fix: Fix a regression that caused Parse Server to crash when a null parameter is passed to a Cloud function
### 2.2.8 (5/8/2016)
* New: Support for Pointer Permissions
* New: Expose logger in Cloud Code
* New: Option to revoke sessions on password reset
* New: Option to expire inactive sessions
* Perf: Improvements in ACL checking query
* Fix: Issues when sending pushes to list of devices that contains invalid values
* Fix: Issues caused by using babel-polyfill outside of Parse Server, but in the same express app
* Fix: Remove creation of extra session tokens
* Fix: Return authData when querying with master key
* Fix: Bugs when deleting webhooks
* Fix: Ignore _RevocableSession header, which might be sent by the JS SDK
* Fix: Issues with querying via URL params
* Fix: Properly encode "Date" parameters to cloud code functions
### 2.2.7 (4/15/2016)
* Adds support for --verbose and verbose option when running ParseServer [\#1414](https://github.com/ParsePlatform/parse-server/pull/1414) ([flovilmart](https://github.com/flovilmart))
* Adds limit = 0 as a valid parameter for queries [\#1493](https://github.com/ParsePlatform/parse-server/pull/1493) ([seijiakiyama](https://github.com/seijiakiyama))
* Makes sure we preserve Installations when updating a token \(\#1475\) [\#1486](https://github.com/ParsePlatform/parse-server/pull/1486) ([flovilmart](https://github.com/flovilmart))
* Hotfix for tests [\#1503](https://github.com/ParsePlatform/parse-server/pull/1503) ([flovilmart](https://github.com/flovilmart))
* Enable logs [\#1502](https://github.com/ParsePlatform/parse-server/pull/1502) ([drew-gross](https://github.com/drew-gross))
* Do some triple equals for great justice [\#1499](https://github.com/ParsePlatform/parse-server/pull/1499) ([TylerBrock](https://github.com/TylerBrock))
* Apply credential stripping to all untransforms for \_User [\#1498](https://github.com/ParsePlatform/parse-server/pull/1498) ([TylerBrock](https://github.com/TylerBrock))
* Checking if object has defined key for Pointer constraints in liveQuery [\#1487](https://github.com/ParsePlatform/parse-server/pull/1487) ([simonas-notcat](https://github.com/simonas-notcat))
* Remove collection prefix and default mongo URI [\#1479](https://github.com/ParsePlatform/parse-server/pull/1479) ([drew-gross](https://github.com/drew-gross))
* Store collection prefix in mongo adapter, and clean up adapter interface [\#1472](https://github.com/ParsePlatform/parse-server/pull/1472) ([drew-gross](https://github.com/drew-gross))
* Move field deletion logic into mongo adapter [\#1471](https://github.com/ParsePlatform/parse-server/pull/1471) ([drew-gross](https://github.com/drew-gross))
* Adds support for Long and Double mongodb types \(fixes \#1316\) [\#1470](https://github.com/ParsePlatform/parse-server/pull/1470) ([flovilmart](https://github.com/flovilmart))
* Schema.js database agnostic [\#1468](https://github.com/ParsePlatform/parse-server/pull/1468) ([flovilmart](https://github.com/flovilmart))
* Remove console.log [\#1465](https://github.com/ParsePlatform/parse-server/pull/1465) ([drew-gross](https://github.com/drew-gross))
* Push status nits [\#1462](https://github.com/ParsePlatform/parse-server/pull/1462) ([flovilmart](https://github.com/flovilmart))
* Fixes \#1444 [\#1451](https://github.com/ParsePlatform/parse-server/pull/1451) ([flovilmart](https://github.com/flovilmart))
* Removing sessionToken and authData from \_User objects included in a query [\#1450](https://github.com/ParsePlatform/parse-server/pull/1450) ([simonas-notcat](https://github.com/simonas-notcat))
* Move mongo field type logic into mongoadapter [\#1432](https://github.com/ParsePlatform/parse-server/pull/1432) ([drew-gross](https://github.com/drew-gross))
* Prevents \_User lock out when setting ACL on signup or afterwards [\#1429](https://github.com/ParsePlatform/parse-server/pull/1429) ([flovilmart](https://github.com/flovilmart))
* Update .travis.yml [\#1428](https://github.com/ParsePlatform/parse-server/pull/1428) ([flovilmart](https://github.com/flovilmart))
* Adds relation fields to objects [\#1424](https://github.com/ParsePlatform/parse-server/pull/1424) ([flovilmart](https://github.com/flovilmart))
* Update .travis.yml [\#1423](https://github.com/ParsePlatform/parse-server/pull/1423) ([flovilmart](https://github.com/flovilmart))
* Sets the defaultSchemas keys in the SchemaCollection [\#1421](https://github.com/ParsePlatform/parse-server/pull/1421) ([flovilmart](https://github.com/flovilmart))
* Fixes \#1417 [\#1420](https://github.com/ParsePlatform/parse-server/pull/1420) ([drew-gross](https://github.com/drew-gross))
* Untransform should treat Array's as nested objects [\#1416](https://github.com/ParsePlatform/parse-server/pull/1416) ([blacha](https://github.com/blacha))
* Adds X-Parse-Push-Status-Id header [\#1412](https://github.com/ParsePlatform/parse-server/pull/1412) ([flovilmart](https://github.com/flovilmart))
* Schema format cleanup [\#1407](https://github.com/ParsePlatform/parse-server/pull/1407) ([drew-gross](https://github.com/drew-gross))
* Updates the publicServerURL option [\#1397](https://github.com/ParsePlatform/parse-server/pull/1397) ([flovilmart](https://github.com/flovilmart))
* Fix exception with non-expiring session tokens. [\#1386](https://github.com/ParsePlatform/parse-server/pull/1386) ([0x18B2EE](https://github.com/0x18B2EE))
* Move mongo schema format related logic into mongo adapter [\#1385](https://github.com/ParsePlatform/parse-server/pull/1385) ([drew-gross](https://github.com/drew-gross))
* WIP: Huge performance improvement on roles queries [\#1383](https://github.com/ParsePlatform/parse-server/pull/1383) ([flovilmart](https://github.com/flovilmart))
* Removes GCS Adapter from provided adapters [\#1339](https://github.com/ParsePlatform/parse-server/pull/1339) ([flovilmart](https://github.com/flovilmart))
* DBController refactoring [\#1228](https://github.com/ParsePlatform/parse-server/pull/1228) ([flovilmart](https://github.com/flovilmart))
* Spotify authentication [\#1226](https://github.com/ParsePlatform/parse-server/pull/1226) ([1nput0utput](https://github.com/1nput0utput))
* Expose DatabaseAdapter to simplify application tests [\#1121](https://github.com/ParsePlatform/parse-server/pull/1121) ([steven-supersolid](https://github.com/steven-supersolid))
### 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))
@@ -20,7 +474,7 @@
* 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)
* 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)
@@ -204,5 +658,3 @@
* Fix: Various files related bugs
* Fix: Force minimum node version 4.3 due to security issues in earlier version
* Performance Improvement: Improved caching
+1
View File
@@ -9,6 +9,7 @@ We really want Parse to be yours, to see it grow and thrive in the open source c
* Take testing seriously! Aim to increase the test coverage with every pull request.
* Run the tests for the file you are working on with `npm test spec/MyFile.spec.js`
* Run the tests for the whole project and look at the coverage report to make sure your tests are exhaustive by running `npm test` and looking at (project-root)/lcov-report/parse-server/FileUnderTest.js.html
* Lint your code by running `npm run lint` to make sure all your code is not gonna be rejected by the CI.
##### Code of Conduct
+21
View File
@@ -0,0 +1,21 @@
FROM node:boron
RUN mkdir -p /parse-server
COPY ./ /parse-server/
RUN mkdir -p /parse-server/config
VOLUME /parse-server/config
RUN mkdir -p /parse-server/cloud
VOLUME /parse-server/cloud
WORKDIR /parse-server
RUN npm install && \
npm run build
ENV PORT=1337
EXPOSE $PORT
ENTRYPOINT ["npm", "start", "--"]
+114 -12
View File
@@ -4,22 +4,34 @@
[![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)
[![Join Chat](https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg)](https://gitter.im/ParsePlatform/Chat)
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.
Parse Server works with the Express web application framework. It can be added to existing web applications, or run by itself.
# Getting Started
April 2016 - We created a series of video screencasts, please check them out here: [http://blog.parse.com/learn/parse-server-video-series-april-2016/](http://blog.parse.com/learn/parse-server-video-series-april-2016/)
The fastest and easiest way to get started is to run MongoDB and Parse Server locally.
## Running Parse Server locally
## Running Parse Server
### Locally
```
$ npm install -g parse-server mongodb-runner
$ mongodb-runner start
$ parse-server --appId APPLICATION_ID --masterKey MASTER_KEY
$ parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://localhost/test
```
### Inside a Docker container
```
$ docker build --tag parse-server .
$ docker run --name my-mongo -d mongo
$ docker run --name my-parse-server --link my-mongo:mongo parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://mongo/test
```
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.
@@ -104,14 +116,15 @@ Once you have a better understanding of how the project works, please refer to t
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://github.com/ParsePlatform/parse-server/wiki/Deploying-Parse-Server#deploying-to-heroku-and-mLab)
* [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/)
* [SashiDo](https://blog.sashido.io/tag/migration/)
* [Digital Ocean](https://www.digitalocean.com/community/tutorials/how-to-run-parse-server-on-ubuntu-14-04)
* [Pivotal Web Services](https://github.com/cf-platform-eng/pws-parse-server)
* [Back4app](http://blog.back4app.com/2016/03/01/quick-wizard-migration/)
* [Gomix](https://gomix.com/#!/project/parse-server)
### Parse Server + Express
@@ -141,13 +154,27 @@ app.listen(1337, function() {
For a full list of available options, run `parse-server --help`.
## Logging
Parse Server will, by default, log:
* to the console
* daily rotating files as new line delimited JSON
Logs are also be viewable in Parse Dashboard.
**Want to log each request and response?** Set the `VERBOSE` environment variable when starting `parse-server`. Usage :- `VERBOSE='1' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY`
**Want logs to be in placed in other folder?** Pass the `PARSE_SERVER_LOGS_FOLDER` environment variable when starting `parse-server`. Usage :- `PARSE_SERVER_LOGS_FOLDER='<path-to-logs-folder>' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY`
**Want new line delimited JSON error logs (for consumption by CloudWatch, Google Cloud Logging, etc.)?** Pass the `JSON_LOGS` environment variable when starting `parse-server`. Usage :- `JSON_LOGS='1' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY`
# Documentation
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.
## 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).
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://parse.com/migration).
## Configuration
@@ -159,7 +186,7 @@ For the full list of available options, run `parse-server --help`.
* `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`.
* `databaseURI` **(required)** - The connection string for your database, i.e. `mongodb://user:pass@host.com/dbname`. Be sure to [URL encode your password](https://app.zencoder.com/docs/guides/getting-started/special-characters-in-usernames-and-passwords) if your password has special characters.
* `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.
@@ -179,26 +206,58 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
* `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).
* `auth` - Used to configure support for [3rd party authentication](https://github.com/ParsePlatform/parse-server/wiki/OAuth--and-Custom-Authentication).
* `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)).
* `databaseAdapter` - The backing store can be changed by creating an adapter class (see `DatabaseAdapter.js`). Defaults to `MongoStorageAdapter`.
* `sessionLength` - The length of time in seconds that a session should be valid for. Defaults to 31536000 seconds (1 year).
* `revokeSessionOnPasswordReset` - When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.
* `accountLockout` - Lock account when a malicious user is attempting to determine an account password by trial and error.
* `passwordPolicy` - Optional password policy rules to enforce.
* `customPages` - A hash with urls to override email verification links, password reset links and specify frame url for masking user-facing pages. Available keys: `parseFrameURL`, `invalidLink`, `choosePassword`, `passwordResetSuccess`, `verifyEmailSuccess`.
* `middleware` - (CLI only), a module name, function that is an express middleware. When using the CLI, the express app will load it just **before** mounting parse-server on the mount path. This option is useful for injecting a monitoring middleware.
##### Logging
Use the `PARSE_SERVER_LOGS_FOLDER` environment variable when starting `parse-server` to save your server logfiles to the specified folder.
Usage:
```
PARSE_SERVER_LOGS_FOLDER='<path-to-logs-folder>' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY
```
##### 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:
Verifying user email addresses and enabling password reset via email requires 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,
// if `verifyUserEmails` is `true` and
// if `emailVerifyTokenValidityDuration` is `undefined` then
// email verify token never expires
// else
// email verify token expires after `emailVerifyTokenValidityDuration`
//
// `emailVerifyTokenValidityDuration` defaults to `undefined`
//
// email verify token below expires in 2 hours (= 2 * 60 * 60 == 7200 seconds)
emailVerifyTokenValidityDuration: 2 * 60 * 60, // in seconds (2 hours = 7200 seconds)
// set preventLoginWithUnverifiedEmail to false to allow user to login without verifying their email
// set preventLoginWithUnverifiedEmail to true to prevent user from login if their email is not verified
preventLoginWithUnverifiedEmail: false, // defaults to false
// 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',
// Set the mount path as it is in serverURL
publicServerURL: 'https://example.com/parse',
// Your apps name. This will appear in the subject and body of the emails that are sent.
appName: 'Parse App',
// The email adapter
@@ -212,11 +271,39 @@ var server = ParseServer({
// Your API key from mailgun.com
apiKey: 'key-mykey',
}
},
// account lockout policy setting (OPTIONAL) - defaults to undefined
// if the account lockout policy is set and there are more than `threshold` number of failed login attempts then the `login` api call returns error code `Parse.Error.OBJECT_NOT_FOUND` with error message `Your account is locked due to multiple failed login attempts. Please try again after <duration> minute(s)`. After `duration` minutes of no login attempts, the application will allow the user to try login again.
accountLockout: {
duration: 5, // duration policy setting determines the number of minutes that a locked-out account remains locked out before automatically becoming unlocked. Set it to a value greater than 0 and less than 100000.
threshold: 3, // threshold policy setting determines the number of failed sign-in attempts that will cause a user account to be locked. Set it to an integer value greater than 0 and less than 1000.
},
// optional settings to enforce password policies
passwordPolicy: {
// Two optional settings to enforce strong passwords. Either one or both can be specified.
// If both are specified, both checks must pass to accept the password
// 1. a RegExp object or a regex string representing the pattern to enforce
validatorPattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/, // enforce password with at least 8 char with at least 1 lower case, 1 upper case and 1 digit
// 2. a callback function to be invoked to validate the password
validatorCallback: (password) => { return validatePassword(password) },
doNotAllowUsername: true, // optional setting to disallow username in passwords
maxPasswordAge: 90, // optional setting in days for password expiry. Login fails if user does not reset the password within this period after signup/last reset.
maxPasswordHistory: 5, // optional setting to prevent reuse of previous n passwords. Maximum value that can be specified is 20. Not specifying it or specifying 0 will not enforce history.
//optional setting to set a validity duration for password reset links (in seconds)
resetTokenValidityDuration: 24*60*60, // expire after 24 hours
}
});
```
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).
You can also use other email adapters contributed by the community such as:
- [parse-server-postmark-adapter](https://www.npmjs.com/package/parse-server-postmark-adapter)
- [parse-server-sendgrid-adapter](https://www.npmjs.com/package/parse-server-sendgrid-adapter)
- [parse-server-mandrill-adapter](https://www.npmjs.com/package/parse-server-mandrill-adapter)
- [parse-server-simple-ses-adapter](https://www.npmjs.com/package/parse-server-simple-ses-adapter)
- [parse-server-mailgun-adapter-template](https://www.npmjs.com/package/parse-server-mailgun-adapter-template)
- [parse-server-mailjet-adapter](https://www.npmjs.com/package/parse-server-mailjet-adapter)
- [simple-parse-smtp-adapter](https://www.npmjs.com/package/simple-parse-smtp-adapter)
### Using environment variables to configure Parse Server
@@ -239,6 +326,9 @@ $ PORT=8080 parse-server --appId APPLICATION_ID --masterKey MASTER_KEY
For the full list of configurable environment variables, run `parse-server --help`.
### Available Adapters
[Parse Server Modules (Adapters)](https://github.com/parse-server-modules)
### Configuring File Adapters
Parse Server allows developers to choose from several options when hosting files:
@@ -261,6 +351,18 @@ If you believe you've found an issue with Parse Server, make sure these boxes ar
- [ ] 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.
# Want to ride the bleeding edge?
The `latest` branch in this repository is automatically maintained to be the last
commit to `master` to pass all tests, in the same form found on npm. It is
recommend to use builds deployed npm for many reasons, but if you want to use
the latest not-yet-released version of parse-server, you can do so by depending
directly on this branch:
```
npm install parseplatform/parse-server.git#latest
```
# 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).
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env node
require("../lib/cli/parse-live-query-server");
+60 -14
View File
@@ -4,7 +4,7 @@ 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'
DEFAULT_MONGODB_URI='mongodb://127.0.0.1:27017/parse'
confirm() {
DEFAULT=$1;
@@ -53,15 +53,60 @@ check_npm() {
echo ''
echo 'This will setup parse-server in the current directory'
echo '
`.-://////:-..`
`:/oooooooooooooooo+:.`
`:+oooooooooooooooooooooo+/`
:+ooooooooooooooooooooooooooo/.
.+oooooooooooooo/:.....-:+ooooooo-
.+ooooooooooooo/` .:///:-` -+oooooo:
`+ooooooooooooo: `/ooooooo+- `ooooooo-
:oooooooooooooo :ooooooooo+` /oooooo+
+ooooooooooooo/ +ooooooooo+ /ooooooo.
oooooooooooooo+ ooooooooo` .oooooooo.
+ooooooooooo+/: `ooooooo` .:ooooooooo.
:ooooooo+.````````````` /+oooooooooo+
`+oooooo- `ooo+ /oooooooooooooooooooo-
.+ooooo/ :/:` -ooooooooooooooooooo:
.+ooooo+:-..-/ooooooooooooooooooo-
:+ooooooooooooooooooooooooooo/.
`:+oooooooooooooooooooooo+/`
`:/oooooooooooooooo+:.`
`.-://////:-..`
parse-server
'
INSTALL_DIR=""
printf "Enter an installation directory\n"
printf "(%s): " "${PWD}"
read -r INSTALL_DIR
if [ "$INSTALL_DIR" = "" ]; then
INSTALL_DIR="${PWD}"
fi
echo ''
printf "This will setup parse-server in %s\n" "${INSTALL_DIR}"
confirm 'Y' 'Do you want to continue? (Y/n): '
check_node
check_npm
echo "Setting up parse-server in $PWD"
printf "Setting up parse-server in %s\n" "${INSTALL_DIR}"
if [ -f './package.json' ]; then
if [ -d "${INSTALL_DIR}" ]; then
echo "${CHECK} ${INSTALL_DIR} exists"
else
mkdir -p "${INSTALL_DIR}"
echo "${CHECK} Created ${INSTALL_DIR}"
fi
cd "${INSTALL_DIR}"
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
@@ -77,33 +122,33 @@ i=0
while [ "$APP_NAME" = "" ]
do
[[ $i != 0 ]] && printf "${RED}An application name is required!${NC}\n"
printf 'Enter your Application Name: '
printf "Enter your ${BOLD}Application Name${NC}: "
read -r APP_NAME
i=$(($i+1))
done
printf 'Enter your appId (leave empty to generate): '
printf "Enter your ${BOLD}Application Id${NC} (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): '
printf "Enter your ${BOLD}Master Key${NC} (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
printf "Enter your ${BOLD}mongodbURI${NC} (%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",
"appId": "${APP_ID}",
"masterKey": "${MASTER_KEY}",
"appName": "${APP_NAME}",
"cloud": "./cloud/main",
"mongodbURI": "$MONGODB_URI"
"databaseURI": "${MONGODB_URI}"
}
EOF
echo "${CHECK} Created config.json"
@@ -113,8 +158,9 @@ NPM_APP_NAME=$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
cat > ./package.json << EOF
{
"name": "$NPM_APP_NAME",
"description": "parse-server for $APP_NAME",
"scripts": {
"start": "parse-server ./config.json"
"start": "parse-server config.json"
},
"dependencies": {
"parse-server": "^2.0.0"
@@ -149,7 +195,7 @@ fi
echo "\n${CHECK} running npm install\n"
npm install
npm install -s
CURL_CMD=$(cat << EOF
curl -X POST -H 'X-Parse-Application-Id: ${APP_ID}' \\
+58 -49
View File
@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "2.2.3",
"version": "2.3.7",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
@@ -18,65 +18,74 @@
],
"license": "BSD-3-Clause",
"dependencies": {
"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",
"lru-cache": "^4.0.0",
"mailgun-js": "^0.7.7",
"mime": "^1.3.4",
"mongodb": "~2.1.0",
"multer": "^1.1.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",
"tv4": "^1.2.7",
"winston": "^2.1.1",
"winston-daily-rotate-file": "^1.0.1",
"ws": "^1.0.1"
"bcryptjs": "2.4.3",
"body-parser": "1.17.1",
"commander": "2.9.0",
"deepcopy": "0.6.3",
"express": "4.15.2",
"intersect": "1.0.1",
"lodash": "4.17.4",
"lru-cache": "4.0.2",
"mime": "1.3.4",
"mongodb": "2.2.24",
"multer": "1.3.0",
"parse": "1.9.2",
"parse-server-fs-adapter": "1.0.1",
"parse-server-push-adapter": "1.2.0",
"parse-server-s3-adapter": "1.0.6",
"parse-server-simple-mailgun-adapter": "1.0.0",
"pg-promise": "5.6.2",
"redis": "2.6.5",
"request": "2.81.0",
"semver": "5.2.0",
"tv4": "1.2.7",
"winston": "2.3.1",
"winston-daily-rotate-file": "1.4.5",
"ws": "2.2.0"
},
"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",
"nodemon": "^1.8.1"
"babel-cli": "6.23.0",
"babel-core": "6.23.1",
"babel-eslint": "^7.1.1",
"babel-plugin-syntax-flow": "6.13.0",
"babel-plugin-transform-flow-strip-types": "6.22.0",
"babel-preset-es2015": "6.22.0",
"babel-preset-stage-0": "6.22.0",
"babel-register": "6.23.0",
"bcrypt-nodejs": "0.0.3",
"cross-env": "3.1.4",
"deep-diff": "0.3.4",
"eslint": "^3.16.1",
"eslint-plugin-flowtype": "^2.25.0",
"gaze": "1.1.1",
"istanbul": "1.0.0-alpha.1",
"jasmine": "2.5.3",
"jasmine-spec-reporter": "^3.1.0",
"mongodb-runner": "3.4.0",
"nodemon": "1.11.0",
"request-promise": "4.1.1"
},
"scripts": {
"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 $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",
"lint": "eslint --cache ./",
"build": "babel src/ -d lib/ --copy-files",
"pretest": "npm run lint",
"test": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 NODE_ENV=test TESTING=1 $COVERAGE_OPTION jasmine",
"test:win": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 NODE_ENV=test TESTING=1 jasmine",
"coverage": "cross-env COVERAGE_OPTION='./node_modules/.bin/istanbul cover' npm test",
"coverage:win": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 NODE_ENV=test TESTING=1 node ./node_modules/istanbul/lib/cli.js cover ./node_modules/jasmine/bin/jasmine.js",
"start": "node ./bin/parse-server",
"prepublish": "npm run build"
},
"engines": {
"node": ">=4.3"
"node": ">=4.6"
},
"bin": {
"parse-server": "./bin/parse-server"
},
"optionalDependencies": {
"bcrypt": "1.0.2",
"uws": "^0.13.0"
}
}
+3
View File
@@ -0,0 +1,3 @@
docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
docker build -t parseplatform/parse-server:$TRAVIS_TAG .
docker push parseplatform/parse-server:$TRAVIS_TAG
+33
View File
@@ -0,0 +1,33 @@
#!/bin/sh -e
# This script maintains a git branch which mirrors master but in a form that
# what will eventually be deployed to npm, allowing npm dependencies to use:
#
# "parse-server": "parseplatform/parse-server#latest"
#
# From: https://github.com/graphql/graphql-js/blob/master/resources/npm-git.sh
BUILD_DIR=latest
npm run build
mkdir -p $BUILD_DIR
cp package.json $BUILD_DIR/
cp README.md $BUILD_DIR/
cp LICENSE $BUILD_DIR/
cp PATENTS $BUILD_DIR/
cp CHANGELOG.md $BUILD_DIR/
cp -R lib $BUILD_DIR
cp -R bin $BUILD_DIR
cp -R public_html $BUILD_DIR
cp -R views $BUILD_DIR
cd $BUILD_DIR
git init
git config user.name "Travis CI"
git config user.email "github@fb.com"
git add .
git commit -m "Deploy master to LATEST branch"
git push --force --quiet "https://${GH_TOKEN}@github.com/parseplatform/parse-server.git" master:latest
+102
View File
@@ -0,0 +1,102 @@
"""
https://github.com/dmakhno/travis_after_all/blob/master/travis_after_all.py
https://github.com/graphql/graphql-js/blob/master/resources/travis_after_all.py
"""
import os
import json
import time
import logging
try:
import urllib.request as urllib2
except ImportError:
import urllib2
log = logging.getLogger("travis.leader")
log.addHandler(logging.StreamHandler())
log.setLevel(logging.INFO)
TRAVIS_JOB_NUMBER = 'TRAVIS_JOB_NUMBER'
TRAVIS_BUILD_ID = 'TRAVIS_BUILD_ID'
POLLING_INTERVAL = 'LEADER_POLLING_INTERVAL'
build_id = os.getenv(TRAVIS_BUILD_ID)
polling_interval = int(os.getenv(POLLING_INTERVAL, '5'))
#assume, first job is the leader
is_leader = lambda job_number: job_number.endswith('.1')
if not os.getenv(TRAVIS_JOB_NUMBER):
# seems even for builds with only one job, this won't get here
log.fatal("Don't use defining leader for build without matrix")
exit(1)
elif is_leader(os.getenv(TRAVIS_JOB_NUMBER)):
log.info("This is a leader")
else:
#since python is subprocess, env variables are exported back via file
with open(".to_export_back", "w") as export_var:
export_var.write("BUILD_MINION=YES")
log.info("This is a minion")
exit(0)
class MatrixElement(object):
def __init__(self, json_raw):
self.is_finished = json_raw['finished_at'] is not None
self.is_succeeded = json_raw['result'] == 0
self.number = json_raw['number']
self.is_leader = is_leader(self.number)
def matrix_snapshot():
"""
:return: Matrix List
"""
response = urllib2.build_opener().open("https://api.travis-ci.org/builds/{0}".format(build_id)).read()
raw_json = json.loads(response)
matrix_without_leader = [MatrixElement(element) for element in raw_json["matrix"]]
return matrix_without_leader
def wait_others_to_finish():
def others_finished():
"""
Dumps others to finish
Leader cannot finish, it is working now
:return: tuple(True or False, List of not finished jobs)
"""
snapshot = matrix_snapshot()
finished = [el.is_finished for el in snapshot if not el.is_leader]
return reduce(lambda a, b: a and b, finished), [el.number for el in snapshot if
not el.is_leader and not el.is_finished]
while True:
finished, waiting_list = others_finished()
if finished: break
log.info("Leader waits for minions {0}...".format(waiting_list)) # just in case do not get "silence timeout"
time.sleep(polling_interval)
try:
wait_others_to_finish()
final_snapshot = matrix_snapshot()
log.info("Final Results: {0}".format([(e.number, e.is_succeeded) for e in final_snapshot]))
BUILD_AGGREGATE_STATUS = 'BUILD_AGGREGATE_STATUS'
others_snapshot = [el for el in final_snapshot if not el.is_leader]
if reduce(lambda a, b: a and b, [e.is_succeeded for e in others_snapshot]):
os.environ[BUILD_AGGREGATE_STATUS] = "others_succeeded"
elif reduce(lambda a, b: a and b, [not e.is_succeeded for e in others_snapshot]):
log.error("Others Failed")
os.environ[BUILD_AGGREGATE_STATUS] = "others_failed"
else:
log.warn("Others Unknown")
os.environ[BUILD_AGGREGATE_STATUS] = "unknown"
#since python is subprocess, env variables are exported back via file
with open(".to_export_back", "w") as export_var:
export_var.write("BUILD_LEADER=YES {0}={1}".format(BUILD_AGGREGATE_STATUS, os.environ[BUILD_AGGREGATE_STATUS]))
except Exception as e:
log.fatal(e)
+31
View File
@@ -0,0 +1,31 @@
{
"env": {
"jasmine": true
},
"globals": {
"Parse": true,
"reconfigureServer": true,
"createTestUser": true,
"jfail": true,
"ok": true,
"strictEqual": true,
"TestObject": true,
"Item": true,
"Container": true,
"equal": true,
"notEqual": true,
"it_exclude_dbs": true,
"describe_only_db": true,
"on_db": true,
"defaultConfiguration": true,
"expectSuccess": true,
"range": true,
"expectError": true,
"jequal": true,
"create": true,
"arrayContains": true
},
"rules": {
"no-console": [0]
}
}
+318
View File
@@ -0,0 +1,318 @@
"use strict";
const Config = require("../src/Config");
var loginWithWrongCredentialsShouldFail = function(username, password) {
return new Promise((resolve, reject) => {
Parse.User.logIn(username, password)
.then(() => reject('login should have failed'))
.catch(err => {
if (err.message === 'Invalid username/password.') {
resolve();
} else {
reject(err);
}
});
});
};
var isAccountLockoutError = function(username, password, duration, waitTime) {
return new Promise((resolve, reject) => {
setTimeout(() => {
Parse.User.logIn(username, password)
.then(() => reject('login should have failed'))
.catch(err => {
if (err.message === 'Your account is locked due to multiple failed login attempts. Please try again after ' + duration + ' minute(s)') {
resolve();
} else {
reject(err);
}
});
}, waitTime);
});
};
describe("Account Lockout Policy: ", () => {
it('account should not be locked even after failed login attempts if account lockout policy is not set', done => {
reconfigureServer({
appName: 'unlimited',
publicServerURL: 'http://localhost:1337/1',
})
.then(() => {
var user = new Parse.User();
user.setUsername('username1');
user.setPassword('password');
return user.signUp(null);
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 1');
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 2');
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 3');
})
.then(() => done())
.catch(err => {
fail('allow unlimited failed login attempts failed: ' + JSON.stringify(err));
done();
});
});
it('throw error if duration is set to an invalid number', done => {
reconfigureServer({
appName: 'duration',
accountLockout: {
duration: 'invalid value',
threshold: 5
},
publicServerURL: "https://my.public.server.com/1"
})
.then(() => {
new Config('test');
fail('set duration to an invalid number test failed');
done();
})
.catch(err => {
if (err && err === 'Account lockout duration should be greater than 0 and less than 100000') {
done();
} else {
fail('set duration to an invalid number test failed: ' + JSON.stringify(err));
done();
}
});
});
it('throw error if threshold is set to an invalid number', done => {
reconfigureServer({
appName: 'threshold',
accountLockout: {
duration: 5,
threshold: 'invalid number'
},
publicServerURL: "https://my.public.server.com/1"
})
.then(() => {
new Config('test');
fail('set threshold to an invalid number test failed');
done();
})
.catch(err => {
if (err && err === 'Account lockout threshold should be an integer greater than 0 and less than 1000') {
done();
} else {
fail('set threshold to an invalid number test failed: ' + JSON.stringify(err));
done();
}
});
});
it('throw error if threshold is < 1', done => {
reconfigureServer({
appName: 'threshold',
accountLockout: {
duration: 5,
threshold: 0
},
publicServerURL: "https://my.public.server.com/1"
})
.then(() => {
new Config('test');
fail('threshold value < 1 is invalid test failed');
done();
})
.catch(err => {
if (err && err === 'Account lockout threshold should be an integer greater than 0 and less than 1000') {
done();
} else {
fail('threshold value < 1 is invalid test failed: ' + JSON.stringify(err));
done();
}
});
});
it('throw error if threshold is > 999', done => {
reconfigureServer({
appName: 'threshold',
accountLockout: {
duration: 5,
threshold: 1000
},
publicServerURL: "https://my.public.server.com/1"
})
.then(() => {
new Config('test');
fail('threshold value > 999 is invalid test failed');
done();
})
.catch(err => {
if (err && err === 'Account lockout threshold should be an integer greater than 0 and less than 1000') {
done();
} else {
fail('threshold value > 999 is invalid test failed: ' + JSON.stringify(err));
done();
}
});
});
it('throw error if duration is <= 0', done => {
reconfigureServer({
appName: 'duration',
accountLockout: {
duration: 0,
threshold: 5
},
publicServerURL: "https://my.public.server.com/1"
})
.then(() => {
new Config('test');
fail('duration value < 1 is invalid test failed');
done();
})
.catch(err => {
if (err && err === 'Account lockout duration should be greater than 0 and less than 100000') {
done();
} else {
fail('duration value < 1 is invalid test failed: ' + JSON.stringify(err));
done();
}
});
});
it('throw error if duration is > 99999', done => {
reconfigureServer({
appName: 'duration',
accountLockout: {
duration: 100000,
threshold: 5
},
publicServerURL: "https://my.public.server.com/1"
})
.then(() => {
new Config('test');
fail('duration value > 99999 is invalid test failed');
done();
})
.catch(err => {
if (err && err === 'Account lockout duration should be greater than 0 and less than 100000') {
done();
} else {
fail('duration value > 99999 is invalid test failed: ' + JSON.stringify(err));
done();
}
});
});
it('lock account if failed login attempts are above threshold', done => {
reconfigureServer({
appName: 'lockout threshold',
accountLockout: {
duration: 1,
threshold: 2
},
publicServerURL: "http://localhost:8378/1"
})
.then(() => {
var user = new Parse.User();
user.setUsername("username2");
user.setPassword("failedLoginAttemptsThreshold");
return user.signUp();
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username2', 'wrong password');
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username2', 'wrong password');
})
.then(() => {
return isAccountLockoutError('username2', 'wrong password', 1, 1);
})
.then(() => {
done();
})
.catch(err => {
fail('lock account after failed login attempts test failed: ' + JSON.stringify(err));
done();
});
});
it('lock account for accountPolicy.duration minutes if failed login attempts are above threshold', done => {
reconfigureServer({
appName: 'lockout threshold',
accountLockout: {
duration: 0.05, // 0.05*60 = 3 secs
threshold: 2
},
publicServerURL: "http://localhost:8378/1"
})
.then(() => {
var user = new Parse.User();
user.setUsername("username3");
user.setPassword("failedLoginAttemptsThreshold");
return user.signUp();
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username3', 'wrong password');
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username3', 'wrong password');
})
.then(() => {
return isAccountLockoutError('username3', 'wrong password', 0.05, 1);
})
.then(() => {
// account should still be locked even after 2 seconds.
return isAccountLockoutError('username3', 'wrong password', 0.05, 2000);
})
.then(() => {
done();
})
.catch(err => {
fail('account should be locked for duration mins test failed: ' + JSON.stringify(err));
done();
});
});
it('allow login for locked account after accountPolicy.duration minutes', done => {
reconfigureServer({
appName: 'lockout threshold',
accountLockout: {
duration: 0.05, // 0.05*60 = 3 secs
threshold: 2
},
publicServerURL: "http://localhost:8378/1"
})
.then(() => {
var user = new Parse.User();
user.setUsername("username4");
user.setPassword("correct password");
return user.signUp();
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username4', 'wrong password');
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username4', 'wrong password');
})
.then(() => {
// allow locked user to login after 3 seconds with a valid userid and password
return new Promise((resolve, reject) => {
setTimeout(() => {
Parse.User.logIn('username4', 'correct password')
.then(() => resolve())
.catch(err => reject(err));
}, 3001);
});
})
.then(() => {
done();
})
.catch(err => {
fail('allow login for locked account after accountPolicy.duration minutes test failed: ' + JSON.stringify(err));
done();
});
});
})
+19 -20
View File
@@ -10,7 +10,6 @@ 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);
@@ -22,7 +21,7 @@ describe("AdaptableController", ()=>{
expect(controller.adapter).toBe(adapter);
done();
});
it("should throw when creating a new mock controller", (done) => {
var adapter = new FilesAdapter();
expect(() => {
@@ -30,9 +29,9 @@ describe("AdaptableController", ()=>{
}).toThrow();
done();
});
it("should fail setting the wrong adapter to the controller", (done) => {
function WrongAdapter() {};
function WrongAdapter() {}
var adapter = new FilesAdapter();
var controller = new FilesController(adapter);
var otherAdapter = new WrongAdapter();
@@ -41,47 +40,47 @@ describe("AdaptableController", ()=>{
}).toThrow();
done();
});
it("should fail to instantiate a controller with wrong adapter", (done) => {
function WrongAdapter() {};
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) { },
createFile: function() { },
deleteFile: function() { },
getFileData: function() { },
getFileLocation: function() { },
}
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) { };
function AGoodAdapter() {}
AGoodAdapter.prototype.createFile = function() { };
AGoodAdapter.prototype.deleteFile = function() { };
AGoodAdapter.prototype.getFileData = function() { };
AGoodAdapter.prototype.getFileLocation = function() { };
var adapter = new AGoodAdapter();
expect(() => {
new FilesController(adapter);
}).not.toThrow();
done();
});
});
});
+38 -10
View File
@@ -2,8 +2,8 @@
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;
const Config = require('../src/Config');
describe("AdapterLoader", ()=>{
@@ -46,6 +46,19 @@ describe("AdapterLoader", ()=>{
done();
});
it("should instantiate an adapter from npm module", (done) => {
var adapter = loadAdapter({
module: 'parse-server-fs-adapter'
});
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
@@ -106,6 +119,30 @@ describe("AdapterLoader", ()=>{
done();
});
it("should load custom push adapter from string (#3544)", (done) => {
var adapterPath = require('path').resolve("./spec/MockPushAdapter");
var options = {
ios: {
bundleId: 'bundle.id'
}
}
const pushAdapterOptions = {
adapter: adapterPath,
options
};
expect(() => {
reconfigureServer({
push: pushAdapterOptions,
}).then(() => {
const config = new Config(Parse.applicationId);
const pushAdapter = config.pushWorker.adapter;
expect(pushAdapter.getValidPushTypes()).toEqual(['ios']);
expect(pushAdapter.options).toEqual(pushAdapterOptions);
done();
});
}).not.toThrow();
});
it("should load S3Adapter from direct passing", (done) => {
var s3Adapter = new S3Adapter("key", "secret", "bucket")
expect(() => {
@@ -114,13 +151,4 @@ describe("AdapterLoader", ()=>{
}).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();
})
});
+61
View File
@@ -0,0 +1,61 @@
const analyticsAdapter = {
appOpened: function() {},
trackEvent: function() {}
}
describe('AnalyticsController', () => {
it('should track a simple event', (done) => {
spyOn(analyticsAdapter, 'trackEvent').and.callThrough();
reconfigureServer({
analyticsAdapter
}).then(() => {
return Parse.Analytics.track('MyEvent', {
key: 'value',
count: '0'
})
}).then(() => {
expect(analyticsAdapter.trackEvent).toHaveBeenCalled();
var lastCall = analyticsAdapter.trackEvent.calls.first();
const args = lastCall.args;
expect(args[0]).toEqual('MyEvent');
expect(args[1]).toEqual({
dimensions: {
key: 'value',
count: '0'
}
});
done();
}, (err) => {
fail(JSON.stringify(err));
done();
})
});
it('should track a app opened event', (done) => {
spyOn(analyticsAdapter, 'appOpened').and.callThrough();
reconfigureServer({
analyticsAdapter
}).then(() => {
return Parse.Analytics.track('AppOpened', {
key: 'value',
count: '0'
})
}).then(() => {
expect(analyticsAdapter.appOpened).toHaveBeenCalled();
var lastCall = analyticsAdapter.appOpened.calls.first();
const args = lastCall.args;
expect(args[0]).toEqual({
dimensions: {
key: 'value',
count: '0'
}
});
done();
}, (err) => {
fail(JSON.stringify(err));
done();
})
})
})
+93
View File
@@ -0,0 +1,93 @@
describe('Auth', () => {
var Auth = require('../src/Auth.js').Auth;
describe('getUserRoles', () => {
var auth;
var config;
var currentRoles = null;
var currentUserId = 'userId';
beforeEach(() => {
currentRoles = ['role:userId'];
config = {
cacheController: {
role: {
get: () => Promise.resolve(currentRoles),
set: jasmine.createSpy('set')
}
}
}
spyOn(config.cacheController.role, 'get').and.callThrough();
auth = new Auth({
config: config,
isMaster: false,
user: {
id: currentUserId
},
installationId: 'installationId'
});
});
it('should get user roles from the cache', (done) => {
auth.getUserRoles()
.then((roles) => {
var firstSet = config.cacheController.role.set.calls.first();
expect(firstSet).toEqual(undefined);
var firstGet = config.cacheController.role.get.calls.first();
expect(firstGet.args[0]).toEqual(currentUserId);
expect(roles).toEqual(currentRoles);
done();
});
});
it('should only query the roles once', (done) => {
var loadRolesSpy = spyOn(auth, '_loadRoles').and.callThrough();
auth.getUserRoles()
.then((roles) => {
expect(roles).toEqual(currentRoles);
return auth.getUserRoles()
})
.then(() => auth.getUserRoles())
.then(() => auth.getUserRoles())
.then((roles) => {
// Should only call the cache adapter once.
expect(config.cacheController.role.get.calls.count()).toEqual(1);
expect(loadRolesSpy.calls.count()).toEqual(1);
var firstGet = config.cacheController.role.get.calls.first();
expect(firstGet.args[0]).toEqual(currentUserId);
expect(roles).toEqual(currentRoles);
done();
});
});
it('should not have any roles with no user', (done) => {
auth.user = null
auth.getUserRoles()
.then((roles) => expect(roles).toEqual([]))
.then(() => done());
});
it('should not have any user roles with master', (done) => {
auth.isMaster = true
auth.getUserRoles()
.then((roles) => expect(roles).toEqual([]))
.then(() => done());
});
it('should properly handle bcrypt upgrade', (done) => {
var bcryptOriginal = require('bcrypt-nodejs');
var bcryptNew = require('bcryptjs');
bcryptOriginal.hash('my1Long:password', null, null, function(err, res) {
bcryptNew.compare('my1Long:password', res, function(err, res) {
expect(res).toBeTruthy();
done();
})
});
});
});
});
+315
View File
@@ -0,0 +1,315 @@
var request = require('request');
var Config = require("../src/Config");
var defaultColumns = require('../src/Controllers/SchemaController').defaultColumns;
var authenticationLoader = require('../src/Adapters/Auth');
var path = require('path');
describe('AuthenticationProviers', function() {
["facebook", "github", "instagram", "google", "linkedin", "meetup", "twitter", "janrainengage", "janraincapture", "vkontakte"].map(function(providerName){
it("Should validate structure of " + providerName, (done) => {
var provider = require("../src/Adapters/Auth/" + 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);
done();
});
});
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);
}
};
};
Parse.User.extend({
extended: function() {
return true;
}
});
var createOAuthUser = function(callback) {
var jsonBody = {
authData: {
myoauth: getMockMyOauthProvider().authData
}
};
var options = {
headers: {'X-Parse-Application-Id': 'test',
'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)
};
return request.post(options, callback);
}
it("should create user with REST API", done => {
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();
var sessionToken = b.sessionToken;
var q = new Parse.Query("_Session");
q.equalTo('sessionToken', sessionToken);
q.first({useMasterKey: true}).then((res) => {
if (!res) {
fail('should not fail fetching the session');
done();
return;
}
expect(res.get("installationId")).toEqual('yolo');
done();
}).fail(() => {
fail('should not fail fetching the session');
done();
})
});
});
it("should only create a single user with REST API", (done) => {
var objectId;
createOAuthUser((error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(b.objectId).not.toBeNull();
expect(b.objectId).not.toBeUndefined();
objectId = b.objectId;
createOAuthUser((error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(b.objectId).not.toBeNull();
expect(b.objectId).not.toBeUndefined();
expect(b.objectId).toBe(objectId);
done();
});
});
});
it("unlink and link with custom provider", (done) => {
var provider = getMockMyOauthProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("myoauth", {
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("myoauth"), "User should be linked to myoauth");
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.adapter.find('_User', {
fields: Object.assign({}, defaultColumns._Default, defaultColumns._Installation),
}, { objectId: 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() {
ok(false, "linking again should succeed");
done();
}
});
});
},
error: function() {
ok(false, "unlinking should succeed");
done();
}
});
},
error: function() {
ok(false, "linking should have worked");
done();
}
});
});
function validateValidator(validator) {
expect(typeof validator).toBe('function');
}
function validateAuthenticationHandler(authenticatonHandler) {
expect(authenticatonHandler).not.toBeUndefined();
expect(typeof authenticatonHandler.getValidatorForProvider).toBe('function');
expect(typeof authenticatonHandler.getValidatorForProvider).toBe('function');
}
function validateAuthenticationAdapter(authAdapter) {
expect(authAdapter).not.toBeUndefined();
if (!authAdapter) { return; }
expect(typeof authAdapter.validateAuthData).toBe('function');
expect(typeof authAdapter.validateAppId).toBe('function');
}
it('properly loads custom adapter', (done) => {
var validAuthData = {
id: 'hello',
token: 'world'
}
const adapter = {
validateAppId: function() {
return Promise.resolve();
},
validateAuthData: function(authData) {
if (authData.id == validAuthData.id && authData.token == validAuthData.token) {
return Promise.resolve();
}
return Promise.reject();
}
};
const authDataSpy = spyOn(adapter, 'validateAuthData').and.callThrough();
const appIdSpy = spyOn(adapter, 'validateAppId').and.callThrough();
const authenticationHandler = authenticationLoader({
customAuthentication: adapter
});
validateAuthenticationHandler(authenticationHandler);
const validator = authenticationHandler.getValidatorForProvider('customAuthentication');
validateValidator(validator);
validator(validAuthData).then(() => {
expect(authDataSpy).toHaveBeenCalled();
// AppIds are not provided in the adapter, should not be called
expect(appIdSpy).not.toHaveBeenCalled();
done();
}, (err) => {
jfail(err);
done();
})
});
it('properly loads custom adapter module object', (done) => {
const authenticationHandler = authenticationLoader({
customAuthentication: path.resolve('./spec/support/CustomAuth.js')
});
validateAuthenticationHandler(authenticationHandler);
const validator = authenticationHandler.getValidatorForProvider('customAuthentication');
validateValidator(validator);
validator({
token: 'my-token'
}).then(() => {
done();
}, (err) => {
jfail(err);
done();
})
});
it('properly loads custom adapter module object', (done) => {
const authenticationHandler = authenticationLoader({
customAuthentication: { module: path.resolve('./spec/support/CustomAuthFunction.js'), options: { token: 'valid-token' }}
});
validateAuthenticationHandler(authenticationHandler);
const validator = authenticationHandler.getValidatorForProvider('customAuthentication');
validateValidator(validator);
validator({
token: 'valid-token'
}).then(() => {
done();
}, (err) => {
jfail(err);
done();
})
});
it('properly loads a default adapter with options', () => {
const options = {
facebook: {
appIds: ['a', 'b']
}
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter('facebook', options);
validateAuthenticationAdapter(adapter);
expect(appIds).toEqual(['a', 'b']);
expect(providerOptions).toEqual(options.facebook);
});
it('properly loads a custom adapter with options', () => {
const options = {
custom: {
validateAppId: () => {},
validateAuthData: () => {},
appIds: ['a', 'b']
}
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter('custom', options);
validateAuthenticationAdapter(adapter);
expect(appIds).toEqual(['a', 'b']);
expect(providerOptions).toEqual(options.custom);
});
});
+146 -54
View File
@@ -1,29 +1,31 @@
var commander = require("../src/cli/utils/commander").default;
'use strict';
import commander from '../src/cli/utils/commander';
import definitions from '../src/cli/definitions/parse-server';
import liveQueryDefinitions from '../src/cli/definitions/parse-live-query-server';
var definitions = {
"arg0": "PROGRAM_ARG_0",
"arg1": {
env: "PROGRAM_ARG_1",
var testDefinitions = {
'arg0': 'PROGRAM_ARG_0',
'arg1': {
env: 'PROGRAM_ARG_1',
required: true
},
"arg2": {
env: "PROGRAM_ARG_2",
'arg2': {
env: 'PROGRAM_ARG_2',
action: function(value) {
var value = parseInt(value);
if (!Number.isInteger(value)) {
throw "port is invalid";
var intValue = parseInt(value);
if (!Number.isInteger(intValue)) {
throw 'arg2 is invalid';
}
return value;
return intValue;
}
},
"arg3": {},
"arg4": {
default: "arg4Value"
'arg3': {},
'arg4': {
default: 'arg4Value'
}
}
};
describe("commander additions", () => {
describe('commander additions', () => {
afterEach((done) => {
commander.options = [];
delete commander.arg0;
@@ -32,56 +34,146 @@ describe("commander additions", () => {
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");
});
it('should load properly definitions from args', (done) => {
commander.loadDefinitions(testDefinitions);
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");
expect(commander.arg3).toEqual('some');
expect(commander.arg4).toEqual('arg4Value');
done();
});
it("should load properly definitions from env", (done) => {
commander.loadDefinitions(definitions);
it('should load properly definitions from env', (done) => {
commander.loadDefinitions(testDefinitions);
commander.parse([], {
"PROGRAM_ARG_0": "arg0ENVValue",
"PROGRAM_ARG_1": "arg1ENVValue",
"PROGRAM_ARG_2": "3",
'PROGRAM_ARG_0': 'arg0ENVValue',
'PROGRAM_ARG_1': 'arg1ENVValue',
'PROGRAM_ARG_2': '3',
});
expect(commander.arg0).toEqual("arg0ENVValue");
expect(commander.arg1).toEqual("arg1ENVValue");
expect(commander.arg0).toEqual('arg0ENVValue');
expect(commander.arg1).toEqual('arg1ENVValue');
expect(commander.arg2).toEqual(3);
expect(commander.arg4).toEqual("arg4Value");
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",
it('should load properly use args over env', (done) => {
commander.loadDefinitions(testDefinitions);
commander.parse(['node','./CLI.spec.js','--arg0', 'arg0Value', '--arg4', ''], {
'PROGRAM_ARG_0': 'arg0ENVValue',
'PROGRAM_ARG_1': 'arg1ENVValue',
'PROGRAM_ARG_2': '4',
'PROGRAM_ARG_4': 'arg4ENVValue'
});
expect(commander.arg0).toEqual("arg0Value");
expect(commander.arg1).toEqual("arg1ENVValue");
expect(commander.arg0).toEqual('arg0Value');
expect(commander.arg1).toEqual('arg1ENVValue');
expect(commander.arg2).toEqual(4);
expect(commander.arg4).toEqual("anotherArg4");
expect(commander.arg4).toEqual('');
done();
});
it("should fail in action as port is invalid", (done) => {
commander.loadDefinitions(definitions);
it('should fail in action as port is invalid', (done) => {
commander.loadDefinitions(testDefinitions);
expect(()=> {
commander.parse(["node","./CLI.spec.js","--arg0", "arg0Value"], {
"PROGRAM_ARG_0": "arg0ENVValue",
"PROGRAM_ARG_1": "arg1ENVValue",
"PROGRAM_ARG_2": "hello",
commander.parse(['node','./CLI.spec.js','--arg0', 'arg0Value'], {
'PROGRAM_ARG_0': 'arg0ENVValue',
'PROGRAM_ARG_1': 'arg1ENVValue',
'PROGRAM_ARG_2': 'hello',
});
}).toThrow("port is invalid");
}).toThrow('arg2 is invalid');
done();
});
});
it('should not override config.json', (done) => {
commander.loadDefinitions(testDefinitions);
commander.parse(['node','./CLI.spec.js','--arg0', 'arg0Value', './spec/configs/CLIConfig.json'], {
'PROGRAM_ARG_0': 'arg0ENVValue',
'PROGRAM_ARG_1': 'arg1ENVValue',
});
const 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(testDefinitions);
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(testDefinitions);
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(testDefinitions);
commander.parse(['node', './CLI.spec.js', './spec/configs/CLIConfigApps.json']);
const 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(testDefinitions);
expect(() => {
commander.parse(['node', './CLI.spec.js', './spec/configs/CLIConfigUnknownArg.json']);
}).toThrow('error: unknown option myArg');
done();
});
});
describe('definitions', () => {
it('should have valid types', () => {
for (const key in definitions) {
const definition = definitions[key];
expect(typeof definition).toBe('object');
if (typeof definition.env !== 'undefined') {
expect(typeof definition.env).toBe('string');
}
expect(typeof definition.help).toBe('string');
if (typeof definition.required !== 'undefined') {
expect(typeof definition.required).toBe('boolean');
}
if (typeof definition.action !== 'undefined') {
expect(typeof definition.action).toBe('function');
}
}
});
});
describe('LiveQuery definitions', () => {
it('should have valid types', () => {
for (const key in liveQueryDefinitions) {
const definition = liveQueryDefinitions[key];
expect(typeof definition).toBe('object');
if (typeof definition.env !== 'undefined') {
expect(typeof definition.env).toBe('string');
}
expect(typeof definition.help).toBe('string');
if (typeof definition.required !== 'undefined') {
expect(typeof definition.required).toBe('boolean');
}
if (typeof definition.action !== 'undefined') {
expect(typeof definition.action).toBe('function');
}
}
});
});
+74
View File
@@ -0,0 +1,74 @@
var CacheController = require('../src/Controllers/CacheController.js').default;
describe('CacheController', function() {
var FakeCacheAdapter;
var FakeAppID = 'foo';
var KEY = 'hello';
beforeEach(() => {
FakeCacheAdapter = {
get: () => Promise.resolve(null),
put: jasmine.createSpy('put'),
del: jasmine.createSpy('del'),
clear: jasmine.createSpy('clear')
}
spyOn(FakeCacheAdapter, 'get').and.callThrough();
});
it('should expose role and user caches', (done) => {
var cache = new CacheController(FakeCacheAdapter, FakeAppID);
expect(cache.role).not.toEqual(null);
expect(cache.role.get).not.toEqual(null);
expect(cache.user).not.toEqual(null);
expect(cache.user.get).not.toEqual(null);
done();
});
['role', 'user'].forEach((cacheName) => {
it('should prefix ' + cacheName + ' cache', () => {
var cache = new CacheController(FakeCacheAdapter, FakeAppID)[cacheName];
cache.put(KEY, 'world');
var firstPut = FakeCacheAdapter.put.calls.first();
expect(firstPut.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':'));
cache.get(KEY);
var firstGet = FakeCacheAdapter.get.calls.first();
expect(firstGet.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':'));
cache.del(KEY);
var firstDel = FakeCacheAdapter.del.calls.first();
expect(firstDel.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':'));
});
});
it('should clear the entire cache', () => {
var cache = new CacheController(FakeCacheAdapter, FakeAppID);
cache.clear();
expect(FakeCacheAdapter.clear.calls.count()).toEqual(1);
cache.user.clear();
expect(FakeCacheAdapter.clear.calls.count()).toEqual(2);
cache.role.clear();
expect(FakeCacheAdapter.clear.calls.count()).toEqual(3);
});
it('should handle cache rejections', (done) => {
FakeCacheAdapter.get = () => Promise.reject();
var cache = new CacheController(FakeCacheAdapter, FakeAppID);
cache.get('foo').then(done, () => {
fail('Promise should not be rejected.');
});
});
});
-1
View File
@@ -2,7 +2,6 @@ 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);
+41
View File
@@ -0,0 +1,41 @@
var ClientSDK = require('../src/ClientSDK');
describe('ClientSDK', () => {
it('should properly parse the SDK versions', () => {
const clientSDKFromVersion = ClientSDK.fromString;
expect(clientSDKFromVersion('i1.1.1')).toEqual({
sdk: 'i',
version: '1.1.1'
});
expect(clientSDKFromVersion('i1')).toEqual({
sdk: 'i',
version: '1'
});
expect(clientSDKFromVersion('apple-tv1.13.0')).toEqual({
sdk: 'apple-tv',
version: '1.13.0'
});
expect(clientSDKFromVersion('js1.9.0')).toEqual({
sdk: 'js',
version: '1.9.0'
});
});
it('should properly sastisfy', () => {
expect(ClientSDK.compatible({
js: '>=1.9.0'
})("js1.9.0")).toBe(true);
expect(ClientSDK.compatible({
js: '>=1.9.0'
})("js2.0.0")).toBe(true);
expect(ClientSDK.compatible({
js: '>=1.9.0'
})("js1.8.0")).toBe(false);
expect(ClientSDK.compatible({
js: '>=1.9.0'
})(undefined)).toBe(true);
})
})
File diff suppressed because it is too large Load Diff
+230
View File
@@ -0,0 +1,230 @@
'use strict';
var LoggerController = require('../src/Controllers/LoggerController').LoggerController;
var WinstonLoggerAdapter = require('../src/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter;
const fs = require('fs');
const loremFile = __dirname + '/support/lorem.txt';
describe("Cloud Code Logger", () => {
let user;
beforeEach(done => {
Parse.User.enableUnsafeCurrentUser();
return reconfigureServer({
// useful to flip to false for fine tuning :).
silent: true,
}).then(() => {
return Parse.User.signUp('tester', 'abc')
.then(loggedInUser => user = loggedInUser)
.then(() => Parse.User.logIn(user.get('username'), 'abc'))
.then(() => done())
});
});
// Note that helpers takes care of logout.
// see helpers.js:afterEach
it("should expose log to functions", done => {
var logController = new LoggerController(new WinstonLoggerAdapter());
Parse.Cloud.define("loggerTest", (req, res) => {
req.log.info('logTest', 'info log', { info: 'some log' });
req.log.error('logTest', 'error log', { error: 'there was an error' });
res.success({});
});
Parse.Cloud.run('loggerTest').then(() => {
return logController.getLogs({ from: Date.now() - 500, size: 1000 });
}).then((res) => {
expect(res.length).not.toBe(0);
const lastLogs = res.slice(0, 3);
const cloudFunctionMessage = lastLogs[0];
const errorMessage = lastLogs[1];
const infoMessage = lastLogs[2];
expect(cloudFunctionMessage.level).toBe('info');
expect(cloudFunctionMessage.params).toEqual({});
expect(cloudFunctionMessage.message).toMatch(/Ran cloud function loggerTest for user [^ ]* with:\n {2}Input: {}\n {2}Result: {}/);
expect(cloudFunctionMessage.functionName).toEqual('loggerTest');
expect(errorMessage.level).toBe('error');
expect(errorMessage.error).toBe('there was an error');
expect(errorMessage.message).toBe('logTest error log');
expect(infoMessage.level).toBe('info');
expect(infoMessage.info).toBe('some log');
expect(infoMessage.message).toBe('logTest info log');
done();
});
});
it('trigger should obfuscate password', done => {
const logController = new LoggerController(new WinstonLoggerAdapter());
Parse.Cloud.beforeSave(Parse.User, (req, res) => {
res.success(req.object);
});
Parse.User.signUp('tester123', 'abc')
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
.then((res) => {
const entry = res[0];
expect(entry.message).not.toMatch(/password":"abc/);
expect(entry.message).toMatch(/\*\*\*\*\*\*\*\*/);
done();
})
.then(null, e => done.fail(e));
});
it("should expose log to trigger", (done) => {
var logController = new LoggerController(new WinstonLoggerAdapter());
Parse.Cloud.beforeSave("MyObject", (req, res) => {
req.log.info('beforeSave MyObject', 'info log', { info: 'some log' });
req.log.error('beforeSave MyObject', 'error log', { error: 'there was an error' });
res.success({});
});
const obj = new Parse.Object('MyObject');
obj.save().then(() => {
return logController.getLogs({ from: Date.now() - 500, size: 1000 })
}).then((res) => {
expect(res.length).not.toBe(0);
const lastLogs = res.slice(0, 3);
const cloudTriggerMessage = lastLogs[0];
const errorMessage = lastLogs[1];
const infoMessage = lastLogs[2];
expect(cloudTriggerMessage.level).toBe('info');
expect(cloudTriggerMessage.triggerType).toEqual('beforeSave');
expect(cloudTriggerMessage.message).toMatch(/beforeSave triggered for MyObject for user [^ ]*\n {2}Input: {}\n {2}Result: {}/);
expect(cloudTriggerMessage.user).toBe(user.id);
expect(errorMessage.level).toBe('error');
expect(errorMessage.error).toBe('there was an error');
expect(errorMessage.message).toBe('beforeSave MyObject error log');
expect(infoMessage.level).toBe('info');
expect(infoMessage.info).toBe('some log');
expect(infoMessage.message).toBe('beforeSave MyObject info log');
done();
});
});
it('should truncate really long lines when asked to', () => {
const logController = new LoggerController(new WinstonLoggerAdapter());
const longString = fs.readFileSync(loremFile, 'utf8');
const truncatedString = logController.truncateLogMessage(longString);
expect(truncatedString.length).toBe(1015); // truncate length + the string '... (truncated)'
});
it('should truncate input and result of long lines', done => {
const logController = new LoggerController(new WinstonLoggerAdapter());
const longString = fs.readFileSync(loremFile, 'utf8');
Parse.Cloud.define('aFunction', (req, res) => {
res.success(req.params);
});
Parse.Cloud.run('aFunction', { longString })
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
.then(logs => {
const log = logs[0];
expect(log.level).toEqual('info');
expect(log.message).toMatch(
/Ran cloud function aFunction for user [^ ]* with:\n {2}Input: {.*?\(truncated\)$/m);
done();
})
.then(null, e => done.fail(e));
});
it('should log an afterSave', done => {
const logController = new LoggerController(new WinstonLoggerAdapter());
Parse.Cloud.afterSave("MyObject", () => { });
new Parse.Object('MyObject')
.save()
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
.then((logs) => {
const log = logs[0];
expect(log.triggerType).toEqual('afterSave');
done();
})
// catch errors - not that the error is actually useful :(
.then(null, e => done.fail(e));
});
it('should log a denied beforeSave', done => {
const logController = new LoggerController(new WinstonLoggerAdapter());
Parse.Cloud.beforeSave("MyObject", (req, res) => {
res.error('uh oh!');
});
new Parse.Object('MyObject')
.save()
.then(
() => done.fail('this is not supposed to succeed'),
() => new Promise(resolve => setTimeout(resolve, 100))
)
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
.then(logs => {
const log = logs[1]; // 0 is the 'uh oh!' from rejection...
expect(log.level).toEqual('error');
expect(log.error).toEqual({ code: 141, message: 'uh oh!' });
done()
});
});
it('should log cloud function success', done => {
const logController = new LoggerController(new WinstonLoggerAdapter());
Parse.Cloud.define('aFunction', (req, res) => {
res.success('it worked!');
});
Parse.Cloud.run('aFunction', { foo: 'bar' })
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
.then(logs => {
const log = logs[0];
expect(log.level).toEqual('info');
expect(log.message).toMatch(
/Ran cloud function aFunction for user [^ ]* with:\n {2}Input: {"foo":"bar"}\n {2}Result: "it worked!/);
done();
});
});
it('should log cloud function failure', done => {
const logController = new LoggerController(new WinstonLoggerAdapter());
Parse.Cloud.define('aFunction', (req, res) => {
res.error('it failed!');
});
Parse.Cloud.run('aFunction', { foo: 'bar' })
.then(null, () => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
.then(logs => {
const log = logs[2];
expect(log.level).toEqual('error');
expect(log.message).toMatch(
/Failed running cloud function aFunction for user [^ ]* with:\n {2}Input: {"foo":"bar"}\n {2}Error: {"code":141,"message":"it failed!"}/);
done();
});
});
xit('should log a changed beforeSave indicating a change', done => {
const logController = new LoggerController(new WinstonLoggerAdapter());
Parse.Cloud.beforeSave("MyObject", (req, res) => {
const myObj = req.object;
myObj.set('aChange', true);
res.success(myObj);
});
new Parse.Object('MyObject')
.save()
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
.then(() => {
// expect the log to indicate that it has changed
/*
Here's what it looks like on parse.com...
Input: {"original":{"clientVersion":"1","createdAt":"2016-06-02T05:29:08.694Z","image":{"__type":"File","name":"tfss-xxxxxxxx.png","url":"http://files.parsetfss.com/xxxxxxxx.png"},"lastScanDate":{"__type":"Date","iso":"2016-06-02T05:28:58.135Z"},"localIdentifier":"XXXXX","objectId":"OFHMX7ZUcI","status":... (truncated)
Result: Update changed to {"object":{"__type":"Pointer","className":"Emoticode","objectId":"ksrq7z3Ehc"},"imageThumb":{"__type":"File","name":"tfss-xxxxxxx.png","url":"http://files.parsetfss.com/xxxxx.png"},"status":"success"}
*/
done();
})
.then(null, e => done.fail(JSON.stringify(e)));
}).pend('needs more work.....');
});
-23
View File
@@ -1,23 +0,0 @@
'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();
});
});
+22 -11
View File
@@ -1,17 +1,28 @@
'use strict';
var DatabaseController = require('../src/Controllers/DatabaseController.js');
var validateQuery = DatabaseController._validateQuery;
let DatabaseController = require('../src/Controllers/DatabaseController');
let MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
describe('DatabaseController', function() {
describe('DatabaseController', () => {
it('can be constructed', done => {
let adapter = new MongoStorageAdapter('mongodb://localhost:27017/test');
let databaseController = new DatabaseController(adapter, {
collectionPrefix: 'test_'
describe('validateQuery', function() {
it('should restructure simple cases of SERVER-13732', (done) => {
var query = {$or: [{a: 1}, {a: 2}], _rperm: {$in: ['a', 'b']}, foo: 3};
validateQuery(query);
expect(query).toEqual({$or: [{a: 1, _rperm: {$in: ['a', 'b']}, foo: 3},
{a: 2, _rperm: {$in: ['a', 'b']}, foo: 3}]});
done();
});
databaseController.connect().then(done, error => {
console.log('error', error.stack);
fail();
it('should reject invalid queries', (done) => {
expect(() => validateQuery({$or: {'a': 1}})).toThrow();
done();
});
it('should accept valid queries', (done) => {
expect(() => validateQuery({$or: [{'a': 1}, {'b': 2}]})).not.toThrow();
done();
});
});
});
+780
View File
@@ -0,0 +1,780 @@
"use strict";
const request = require('request');
const requestp = require('request-promise');
const Config = require('../src/Config');
describe("Email Verification Token Expiration: ", () => {
it('show the invalid link page, if the user clicks on the verify email link after the email verify token expires', done => {
var user = new Parse.User();
var sendEmailOptions;
var emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 0.5, // 0.5 second
publicServerURL: "http://localhost:8378/1"
})
.then(() => {
user.setUsername("testEmailVerifyTokenValidity");
user.setPassword("expiringToken");
user.set('email', 'user@parse.com');
return user.signUp();
}).then(() => {
// wait for 1 second - simulate user behavior to some extent
setTimeout(() => {
expect(sendEmailOptions).not.toBeUndefined();
request.get(sendEmailOptions.link, {
followRedirect: false,
}, (error, response) => {
expect(response.statusCode).toEqual(302);
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
done();
});
}, 1000);
}).catch((err) => {
jfail(err);
done();
});
});
it('emailVerified should set to false, if the user does not verify their email before the email verify token expires', done => {
var user = new Parse.User();
var sendEmailOptions;
var emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 0.5, // 0.5 second
publicServerURL: "http://localhost:8378/1"
})
.then(() => {
user.setUsername("testEmailVerifyTokenValidity");
user.setPassword("expiringToken");
user.set('email', 'user@parse.com');
return user.signUp();
}).then(() => {
// wait for 1 second - simulate user behavior to some extent
setTimeout(() => {
expect(sendEmailOptions).not.toBeUndefined();
request.get(sendEmailOptions.link, {
followRedirect: false,
}, (error, response) => {
expect(response.statusCode).toEqual(302);
user.fetch()
.then(() => {
expect(user.get('emailVerified')).toEqual(false);
done();
})
.catch(() => {
jfail(error);
done();
});
});
}, 1000);
}).catch((error) => {
jfail(error);
done();
});
});
it('if user clicks on the email verify link before email verification token expiration then show the verify email success page', done => {
var user = new Parse.User();
var sendEmailOptions;
var emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 5, // 5 seconds
publicServerURL: "http://localhost:8378/1"
})
.then(() => {
user.setUsername("testEmailVerifyTokenValidity");
user.setPassword("expiringToken");
user.set('email', 'user@parse.com');
return user.signUp();
}).then(() => {
request.get(sendEmailOptions.link, {
followRedirect: false,
}, (error, response) => {
expect(response.statusCode).toEqual(302);
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=testEmailVerifyTokenValidity');
done();
});
}).catch((error) => {
jfail(error);
done();
});
});
it('if user clicks on the email verify link before email verification token expiration then emailVerified should be true', done => {
var user = new Parse.User();
var sendEmailOptions;
var emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 5, // 5 seconds
publicServerURL: "http://localhost:8378/1"
})
.then(() => {
user.setUsername("testEmailVerifyTokenValidity");
user.setPassword("expiringToken");
user.set('email', 'user@parse.com');
return user.signUp();
}).then(() => {
request.get(sendEmailOptions.link, {
followRedirect: false,
}, (error, response) => {
expect(response.statusCode).toEqual(302);
user.fetch()
.then(() => {
expect(user.get('emailVerified')).toEqual(true);
done();
})
.catch((error) => {
jfail(error);
done();
});
});
}).catch((error) => {
jfail(error);
done();
});
});
it('if user clicks on the email verify link before email verification token expiration then user should be able to login', done => {
var user = new Parse.User();
var sendEmailOptions;
var emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 5, // 5 seconds
publicServerURL: "http://localhost:8378/1"
})
.then(() => {
user.setUsername("testEmailVerifyTokenValidity");
user.setPassword("expiringToken");
user.set('email', 'user@parse.com');
return user.signUp();
}).then(() => {
request.get(sendEmailOptions.link, {
followRedirect: false,
}, (error, response) => {
expect(response.statusCode).toEqual(302);
Parse.User.logIn("testEmailVerifyTokenValidity", "expiringToken")
.then(user => {
expect(typeof user).toBe('object');
expect(user.get('emailVerified')).toBe(true);
done();
})
.catch((error) => {
jfail(error);
done();
});
});
}).catch((error) => {
jfail(error);
done();
});
});
it('sets the _email_verify_token_expires_at and _email_verify_token fields after user SignUp', done => {
var user = new Parse.User();
var sendEmailOptions;
var emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 5, // 5 seconds
publicServerURL: 'http://localhost:8378/1'
})
.then(() => {
user.setUsername('sets_email_verify_token_expires_at');
user.setPassword('expiringToken');
user.set('email', 'user@parse.com');
return user.signUp();
})
.then(() => {
const config = new Config('test');
return config.database.find('_User', {username: 'sets_email_verify_token_expires_at'});
})
.then(results => {
expect(results.length).toBe(1);
const user = results[0];
expect(typeof user).toBe('object');
expect(user.emailVerified).toEqual(false);
expect(typeof user._email_verify_token).toBe('string');
expect(typeof user._email_verify_token_expires_at).toBe('object');
expect(sendEmailOptions).toBeDefined();
done();
})
.catch(error => {
jfail(error);
done();
});
});
it('unsets the _email_verify_token_expires_at and _email_verify_token fields in the User class if email verification is successful', done => {
var user = new Parse.User();
var sendEmailOptions;
var emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 5, // 5 seconds
publicServerURL: "http://localhost:8378/1"
})
.then(() => {
user.setUsername("unsets_email_verify_token_expires_at");
user.setPassword("expiringToken");
user.set('email', 'user@parse.com');
return user.signUp();
})
.then(() => {
request.get(sendEmailOptions.link, {
followRedirect: false,
}, (error, response) => {
expect(response.statusCode).toEqual(302);
const config = new Config('test');
return config.database.find('_User', {username: 'unsets_email_verify_token_expires_at'}).then((results) => {
expect(results.length).toBe(1);
return results[0];
})
.then(user => {
expect(typeof user).toBe('object');
expect(user.emailVerified).toEqual(true);
expect(typeof user._email_verify_token).toBe('undefined');
expect(typeof user._email_verify_token_expires_at).toBe('undefined');
done();
})
.catch(error => {
jfail(error);
done();
});
});
})
.catch(error => {
jfail(error);
done();
});
});
it('clicking on the email verify link by an email VERIFIED user that was setup before enabling the expire email verify token should show an invalid link', done => {
var user = new Parse.User();
var sendEmailOptions;
var emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
var serverConfig = {
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
};
// setup server WITHOUT enabling the expire email verify token flag
reconfigureServer(serverConfig)
.then(() => {
user.setUsername("testEmailVerifyTokenValidity");
user.setPassword("expiringToken");
user.set('email', 'user@parse.com');
return user.signUp();
})
.then(() => {
return new Promise((resolve, reject) => {
request.get(sendEmailOptions.link, { followRedirect: false, })
.on('error', error => reject(error))
.on('response', (response) => {
expect(response.statusCode).toEqual(302);
resolve(user.fetch());
});
});
})
.then(() => {
expect(user.get('emailVerified')).toEqual(true);
// RECONFIGURE the server i.e., ENABLE the expire email verify token flag
serverConfig.emailVerifyTokenValidityDuration = 5; // 5 seconds
return reconfigureServer(serverConfig);
})
.then(() => {
request.get(sendEmailOptions.link, {
followRedirect: false,
}, (error, response) => {
expect(response.statusCode).toEqual(302);
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
done();
});
})
.catch((error) => {
jfail(error);
done();
});
});
it('clicking on the email verify link by an email UNVERIFIED user that was setup before enabling the expire email verify token should show an invalid link', done => {
var user = new Parse.User();
var sendEmailOptions;
var emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
var serverConfig = {
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
};
// setup server WITHOUT enabling the expire email verify token flag
reconfigureServer(serverConfig)
.then(() => {
user.setUsername("testEmailVerifyTokenValidity");
user.setPassword("expiringToken");
user.set('email', 'user@parse.com');
return user.signUp();
})
.then(() => {
// just get the user again - DO NOT email verify the user
return user.fetch();
})
.then(() => {
expect(user.get('emailVerified')).toEqual(false);
// RECONFIGURE the server i.e., ENABLE the expire email verify token flag
serverConfig.emailVerifyTokenValidityDuration = 5; // 5 seconds
return reconfigureServer(serverConfig);
})
.then(() => {
request.get(sendEmailOptions.link, {
followRedirect: false,
}, (error, response) => {
expect(response.statusCode).toEqual(302);
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
done();
});
})
.catch((error) => {
jfail(error);
done();
});
});
it('setting the email on the user should set a new email verification token and new expiration date for the token when expire email verify token flag is set', done => {
const user = new Parse.User();
let userBeforeEmailReset;
let sendEmailOptions;
const emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
};
const serverConfig = {
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 5, // 5 seconds
publicServerURL: "http://localhost:8378/1"
};
reconfigureServer(serverConfig)
.then(() => {
user.setUsername("newEmailVerifyTokenOnEmailReset");
user.setPassword("expiringToken");
user.set('email', 'user@parse.com');
return user.signUp();
})
.then(() => {
const config = new Config('test');
return config.database.find('_User', {username: 'newEmailVerifyTokenOnEmailReset'}).then((results) => {
return results[0];
});
})
.then(userFromDb => {
expect(typeof userFromDb).toBe('object');
userBeforeEmailReset = userFromDb;
// trigger another token generation by setting the email
user.set('email', 'user@parse.com');
return new Promise((resolve) => {
// wait for half a sec to get a new expiration time
setTimeout(() => resolve(user.save()), 500);
});
})
.then(() => {
const config = new Config('test');
return config.database.find('_User', {username: 'newEmailVerifyTokenOnEmailReset'}).then((results) => {
return results[0];
});
})
.then(userAfterEmailReset => {
expect(typeof userAfterEmailReset).toBe('object');
expect(userBeforeEmailReset._email_verify_token).not.toEqual(userAfterEmailReset._email_verify_token);
expect(userBeforeEmailReset._email_verify_token_expires_at).not.toEqual(userAfterEmailReset.__email_verify_token_expires_at);
expect(sendEmailOptions).toBeDefined();
done();
})
.catch((error) => {
jfail(error);
done();
});
});
it('should send a new verification email when a resend is requested and the user is UNVERIFIED', done => {
var user = new Parse.User();
var sendEmailOptions;
var sendVerificationEmailCallCount = 0;
var emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
sendVerificationEmailCallCount++;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 5, // 5 seconds
publicServerURL: 'http://localhost:8378/1'
})
.then(() => {
user.setUsername('resends_verification_token');
user.setPassword('expiringToken');
user.set('email', 'user@parse.com');
return user.signUp();
})
.then(() => {
expect(sendVerificationEmailCallCount).toBe(1);
return requestp.post({
uri: 'http://localhost:8378/1/verificationEmailRequest',
body: {
email: 'user@parse.com'
},
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
},
json: true,
resolveWithFullResponse: true,
simple: false // this promise is only rejected if the call itself failed
})
.then((response) => {
expect(response.statusCode).toBe(200);
expect(sendVerificationEmailCallCount).toBe(2);
expect(sendEmailOptions).toBeDefined();
done();
});
})
.catch(error => {
jfail(error);
done();
});
});
it('should not send a new verification email when a resend is requested and the user is VERIFIED', done => {
var user = new Parse.User();
var sendEmailOptions;
var sendVerificationEmailCallCount = 0;
var emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
sendVerificationEmailCallCount++;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 5, // 5 seconds
publicServerURL: 'http://localhost:8378/1'
})
.then(() => {
user.setUsername('no_new_verification_token_once_verified');
user.setPassword('expiringToken');
user.set('email', 'user@parse.com');
return user.signUp();
})
.then(() => {
return requestp.get({
url: sendEmailOptions.link,
followRedirect: false,
resolveWithFullResponse: true,
simple: false
})
.then((response) => {
expect(response.statusCode).toEqual(302);
});
})
.then(() => {
expect(sendVerificationEmailCallCount).toBe(1);
return requestp.post({
uri: 'http://localhost:8378/1/verificationEmailRequest',
body: {
email: 'user@parse.com'
},
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
},
json: true,
resolveWithFullResponse: true,
simple: false // this promise is only rejected if the call itself failed
})
.then((response) => {
expect(response.statusCode).toBe(400);
expect(sendVerificationEmailCallCount).toBe(1);
done();
});
})
.catch(error => {
jfail(error);
done();
});
});
it('should not send a new verification email if this user does not exist', done => {
var sendEmailOptions;
var sendVerificationEmailCallCount = 0;
var emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
sendVerificationEmailCallCount++;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 5, // 5 seconds
publicServerURL: 'http://localhost:8378/1'
})
.then(() => {
return requestp.post({
uri: 'http://localhost:8378/1/verificationEmailRequest',
body: {
email: 'user@parse.com'
},
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
},
json: true,
resolveWithFullResponse: true,
simple: false
})
.then(response => {
expect(response.statusCode).toBe(400);
expect(sendVerificationEmailCallCount).toBe(0);
expect(sendEmailOptions).not.toBeDefined();
done();
});
})
.catch(error => {
jfail(error);
done();
});
});
it('should fail if no email is supplied', done => {
var sendEmailOptions;
var sendVerificationEmailCallCount = 0;
var emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
sendVerificationEmailCallCount++;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 5, // 5 seconds
publicServerURL: 'http://localhost:8378/1'
})
.then(() => {
request.post({
uri: 'http://localhost:8378/1/verificationEmailRequest',
body: {},
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
},
json: true,
resolveWithFullResponse: true,
simple: false
}, (err, response) => {
expect(response.statusCode).toBe(400);
expect(response.body.code).toBe(Parse.Error.EMAIL_MISSING);
expect(response.body.error).toBe('you must provide an email');
expect(sendVerificationEmailCallCount).toBe(0);
expect(sendEmailOptions).not.toBeDefined();
done();
});
})
.catch(error => {
jfail(error);
done();
});
});
it('should fail if email is not a string', done => {
var sendEmailOptions;
var sendVerificationEmailCallCount = 0;
var emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
sendVerificationEmailCallCount++;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 5, // 5 seconds
publicServerURL: 'http://localhost:8378/1'
})
.then(() => {
request.post({
uri: 'http://localhost:8378/1/verificationEmailRequest',
body: {email: 3},
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
},
json: true,
resolveWithFullResponse: true,
simple: false
}, (err, response) => {
expect(response.statusCode).toBe(400);
expect(response.body.code).toBe(Parse.Error.INVALID_EMAIL_ADDRESS);
expect(response.body.error).toBe('you must provide a valid email string');
expect(sendVerificationEmailCallCount).toBe(0);
expect(sendEmailOptions).not.toBeDefined();
done();
});
})
.catch(error => {
jfail(error);
done();
});
});
it('client should not see the _email_verify_token_expires_at field', done => {
var user = new Parse.User();
var sendEmailOptions;
var emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 5, // 5 seconds
publicServerURL: "http://localhost:8378/1"
})
.then(() => {
user.setUsername("testEmailVerifyTokenValidity");
user.setPassword("expiringToken");
user.set('email', 'user@parse.com');
return user.signUp();
})
.then(() => {
user.fetch()
.then(() => {
expect(user.get('emailVerified')).toEqual(false);
expect(typeof user.get('_email_verify_token_expires_at')).toBe('undefined');
expect(sendEmailOptions).toBeDefined();
done();
})
.catch(error => {
jfail(error);
done();
});
}).catch((error) => {
jfail(error);
done();
});
});
})
+49
View File
@@ -0,0 +1,49 @@
const auth = require('../src/Auth');
const Config = require('../src/Config');
const rest = require('../src/rest');
describe('Enable single schema cache', () => {
beforeEach((done) => {
reconfigureServer({
enableSingleSchemaCache: true,
schemaCacheTTL: 30000
}).then(() => {
done();
});
});
it('can perform multiple create and query operations', (done) => {
let config = fakeRequestForConfig();
let nobody = auth.nobody(config);
rest.create(config, nobody, 'Foo', {type: 1}).then(() => {
config = fakeRequestForConfig();
nobody = auth.nobody(config);
return rest.create(config, nobody, 'Foo', {type: 2});
}).then(() => {
config = fakeRequestForConfig();
nobody = auth.nobody(config);
return rest.create(config, nobody, 'Bar');
}).then(() => {
config = fakeRequestForConfig();
nobody = auth.nobody(config);
return rest.find(config, nobody, 'Bar', {type: 1});
}).then(() => {
fail('Should throw error');
done();
}, (error) => {
config = fakeRequestForConfig();
nobody = auth.nobody(config);
expect(error).toBeDefined();
return rest.find(config, nobody, 'Foo', {type: 1});
}).then((response) => {
config = fakeRequestForConfig();
nobody = auth.nobody(config);
expect(response.results.length).toEqual(1);
done();
});
});
});
const fakeRequestForConfig = function() {
return new Config('test');
};
+2 -3
View File
@@ -1,7 +1,6 @@
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
var EventEmitterPubSub = require('../src/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub;
describe('EventEmitterPubSub', function() {
it('can publish and subscribe', function() {
var publisher = EventEmitterPubSub.createPublisher();
var subscriber = EventEmitterPubSub.createSubscriber();
@@ -26,7 +25,7 @@ describe('EventEmitterPubSub', function() {
subscriber.unsubscribe('testChannel');
// Register mock checked for subscriber
var isCalled = false;
subscriber.on('message', function(channel, message) {
subscriber.on('message', function() {
isCalled = true;
});
-45
View File
@@ -1,45 +0,0 @@
var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter;
var Parse = require('parse/node').Parse;
describe('info logs', () => {
it("Verify INFO logs", (done) => {
var fileLoggerAdapter = new FileLoggerAdapter();
fileLoggerAdapter.info('testing info logs', () => {
fileLoggerAdapter.query({
size: 1,
level: 'info'
}, (results) => {
if(results.length == 0) {
fail('The adapter should return non-empty results');
done();
} else {
expect(results[0].message).toEqual('testing info logs');
done();
}
});
});
});
});
describe('error logs', () => {
it("Verify ERROR logs", (done) => {
var fileLoggerAdapter = new FileLoggerAdapter();
fileLoggerAdapter.error('testing error logs', () => {
fileLoggerAdapter.query({
size: 1,
level: 'error'
}, (results) => {
if(results.length == 0) {
fail('The adapter should return non-empty results');
done();
}
else {
expect(results[0].message).toEqual('testing error logs');
done();
}
});
});
});
});
+39 -4
View File
@@ -1,7 +1,17 @@
var GridStoreAdapter = require("../src/Adapters/Files/GridStoreAdapter").GridStoreAdapter;
var Config = require("../src/Config");
var FilesController = require('../src/Controllers/FilesController').default;
const LoggerController = require('../src/Controllers/LoggerController').LoggerController;
const WinstonLoggerAdapter = require('../src/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter;
const GridStoreAdapter = require("../src/Adapters/Files/GridStoreAdapter").GridStoreAdapter;
const Config = require("../src/Config");
const FilesController = require('../src/Controllers/FilesController').default;
const mockAdapter = {
createFile: () => {
return Parse.Promise.reject(new Error('it failed'));
},
deleteFile: () => { },
getFileData: () => { },
getFileLocation: () => 'xyz'
}
// Small additional tests to improve overall coverage
describe("FilesController",() =>{
@@ -26,5 +36,30 @@ describe("FilesController",() =>{
expect(anObject.aFile.url).toEqual("http://an.url");
done();
})
});
it('should create a server log on failure', done => {
const logController = new LoggerController(new WinstonLoggerAdapter());
reconfigureServer({ filesAdapter: mockAdapter })
.then(() => new Promise(resolve => setTimeout(resolve, 1000)))
.then(() => new Parse.File("yolo.txt", [1,2,3], "text/plain").save())
.then(
() => done.fail('should not succeed'),
() => setImmediate(() => Parse.Promise.as('done'))
)
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
.then((logs) => {
// we get two logs here: 1. the source of the failure to save the file
// and 2 the message that will be sent back to the client.
const log1 = logs.pop();
expect(log1.level).toBe('error');
expect(log1.message).toBe('it failed');
const log2 = logs.pop();
expect(log2.level).toBe('error');
expect(log2.code).toBe(130);
expect(log2.message).toBe('Could not store file.');
done();
});
});
});
+87
View File
@@ -0,0 +1,87 @@
var MongoClient = require("mongodb").MongoClient;
var GridStore = require("mongodb").GridStore;
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_only_db('mongo')("GridStoreAdapter",() =>{
it("should properly instanciate the GridStore when deleting a file", (done) => {
var databaseURI = 'mongodb://localhost:27017/parse';
var config = new Config(Parse.applicationId);
var gridStoreAdapter = new GridStoreAdapter(databaseURI);
var filesController = new FilesController(gridStoreAdapter);
// save original unlink before redefinition
var originalUnlink = GridStore.prototype.unlink;
var gridStoreMode;
// new unlink method that will capture the mode in which GridStore was opened
GridStore.prototype.unlink = function() {
// restore original unlink during first call
GridStore.prototype.unlink = originalUnlink;
gridStoreMode = this.mode;
return originalUnlink.call(this);
};
filesController.createFile(config, 'myFilename.txt', 'my file content', 'text/plain')
.then(myFile => {
return MongoClient.connect(databaseURI)
.then(database => {
// Verify the existance of the fs.files document
return database.collection('fs.files').count().then(count => {
expect(count).toEqual(1);
return database;
});
})
.then(database => {
// Verify the existance of the fs.files document
return database.collection('fs.chunks').count().then(count => {
expect(count).toEqual(1);
return database.close();
});
})
.then(() => {
return filesController.deleteFile(config, myFile.name);
});
})
.then(() => {
return MongoClient.connect(databaseURI)
.then(database => {
// Verify the existance of the fs.files document
return database.collection('fs.files').count().then(count => {
expect(count).toEqual(0);
return database;
});
})
.then(database => {
// Verify the existance of the fs.files document
return database.collection('fs.chunks').count().then(count => {
expect(count).toEqual(0);
return database.close();
});
});
})
.then(() => {
// Verify that gridStore was opened in read only mode
expect(gridStoreMode).toEqual('r');
done();
})
.catch(fail);
})
});
+120 -54
View File
@@ -1,11 +1,12 @@
'use strict';
var httpRequest = require("../src/cloud-code/httpRequest"),
bodyParser = require('body-parser'),
express = require("express");
HTTPResponse = require('../src/cloud-code/HTTPResponse').default,
bodyParser = require('body-parser'),
express = require("express");
var port = 13371;
var httpRequestServer = "http://localhost:"+port;
var httpRequestServer = "http://localhost:" + port;
var app = express();
app.use(bodyParser.json({ 'type': '*/*' }));
@@ -36,10 +37,9 @@ app.listen(13371);
describe("httpRequest", () => {
it("should do /hello", (done) => {
httpRequest({
url: httpRequestServer+"/hello"
url: httpRequestServer + "/hello"
}).then(function(httpResponse){
expect(httpResponse.status).toBe(200);
expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}'));
@@ -51,11 +51,11 @@ describe("httpRequest", () => {
done();
})
});
it("should do /hello with callback and promises", (done) => {
var calls = 0;
httpRequest({
url: httpRequestServer+"/hello",
url: httpRequestServer + "/hello",
success: function() { calls++; },
error: function() { calls++; }
}).then(function(httpResponse){
@@ -70,11 +70,11 @@ describe("httpRequest", () => {
done();
})
});
it("should do not follow redirects by default", (done) => {
httpRequest({
url: httpRequestServer+"/301"
url: httpRequestServer + "/301"
}).then(function(httpResponse){
expect(httpResponse.status).toBe(301);
done();
@@ -83,11 +83,11 @@ describe("httpRequest", () => {
done();
})
});
it("should follow redirects when set", (done) => {
httpRequest({
url: httpRequestServer+"/301",
url: httpRequestServer + "/301",
followRedirects: true
}).then(function(httpResponse){
expect(httpResponse.status).toBe(200);
@@ -100,17 +100,17 @@ describe("httpRequest", () => {
done();
})
});
it("should fail on 404", (done) => {
var calls = 0;
httpRequest({
url: httpRequestServer+"/404",
success: function() {
url: httpRequestServer + "/404",
success: function() {
calls++;
fail("should not succeed");
done();
},
error: function(httpResponse) {
error: function(httpResponse) {
calls++;
expect(calls).toBe(1);
expect(httpResponse.status).toBe(404);
@@ -121,11 +121,11 @@ describe("httpRequest", () => {
}
});
})
it("should fail on 404", (done) => {
httpRequest({
url: httpRequestServer+"/404",
}).then(function(httpResponse){
url: httpRequestServer + "/404",
}).then(function(){
fail("should not succeed");
done();
}, function(httpResponse){
@@ -136,14 +136,14 @@ describe("httpRequest", () => {
done();
})
})
it("should post on echo", (done) => {
var calls = 0;
httpRequest({
method: "POST",
url: httpRequestServer+"/echo",
url: httpRequestServer + "/echo",
body: {
foo: "bar"
foo: "bar"
},
headers: {
'Content-Type': 'application/json'
@@ -155,48 +155,48 @@ describe("httpRequest", () => {
expect(httpResponse.status).toBe(200);
expect(httpResponse.data).toEqual({foo: "bar"});
done();
}, function(httpResponse){
}, function(){
fail("should not fail");
done();
})
});
it("should encode a query string body by default", (done) => {
let options = {
body: {"foo": "bar"},
const options = {
body: {"foo": "bar"},
}
let result = httpRequest.encodeBody(options);
const 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"},
const options = {
body: {"foo": "bar"},
headers: {'Content-Type': 'application/json'}
}
let result = httpRequest.encodeBody(options);
const result = httpRequest.encodeBody(options);
expect(result.body).toEqual('{"foo":"bar"}');
done();
})
it("should encode a www-form body", (done) => {
let options = {
it("should encode a www-form body", (done) => {
const options = {
body: {"foo": "bar", "bar": "baz"},
headers: {'cOntent-tYpe': 'application/x-www-form-urlencoded'}
}
let result = httpRequest.encodeBody(options);
const 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"},
const options = {
body:{"foo": "bar", "bar": "baz"},
headers: {'cOntent-tYpe': 'mime/jpeg'}
}
let result = httpRequest.encodeBody(options);
const result = httpRequest.encodeBody(options);
expect(result.body).toEqual({"foo": "bar", "bar": "baz"});
done();
});
@@ -204,34 +204,23 @@ describe("httpRequest", () => {
it("should fail gracefully", (done) => {
httpRequest({
url: "http://not a good url",
success: function() {
success: function() {
fail("should not succeed");
done();
},
error: function(error) {
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",
url: httpRequestServer + "/qs",
params: {
foo: "bar"
foo: "bar"
}
}).then(function(httpResponse){
expect(httpResponse.status).toBe(200);
@@ -245,7 +234,7 @@ describe("httpRequest", () => {
it("should params string to query string", (done) => {
httpRequest({
url: httpRequestServer+"/qs",
url: httpRequestServer + "/qs",
params: "foo=bar&foo2=bar2"
}).then(function(httpResponse){
expect(httpResponse.status).toBe(200);
@@ -257,4 +246,81 @@ describe("httpRequest", () => {
})
});
it('should not crash with undefined body', () => {
const httpResponse = new HTTPResponse({});
expect(httpResponse.body).toBeUndefined();
expect(httpResponse.data).toBeUndefined();
expect(httpResponse.text).toBeUndefined();
expect(httpResponse.buffer).toBeUndefined();
});
it('serialized httpResponse correctly with body string', () => {
const httpResponse = new HTTPResponse({}, 'hello');
expect(httpResponse.text).toBe('hello');
expect(httpResponse.data).toBe(undefined);
expect(httpResponse.body).toBe('hello');
const serialized = JSON.stringify(httpResponse);
const result = JSON.parse(serialized);
expect(result.text).toBe('hello');
expect(result.data).toBe(undefined);
expect(result.body).toBe(undefined);
});
it('serialized httpResponse correctly with body object', () => {
const httpResponse = new HTTPResponse({}, {foo: "bar"});
Parse._encode(httpResponse);
const serialized = JSON.stringify(httpResponse);
const result = JSON.parse(serialized);
expect(httpResponse.text).toEqual('{"foo":"bar"}');
expect(httpResponse.data).toEqual({foo: 'bar'});
expect(httpResponse.body).toEqual({foo: 'bar'});
expect(result.text).toEqual('{"foo":"bar"}');
expect(result.data).toEqual({foo: 'bar'});
expect(result.body).toEqual(undefined);
});
it('serialized httpResponse correctly with body buffer string', () => {
const httpResponse = new HTTPResponse({}, new Buffer('hello'));
expect(httpResponse.text).toBe('hello');
expect(httpResponse.data).toBe(undefined);
const serialized = JSON.stringify(httpResponse);
const result = JSON.parse(serialized);
expect(result.text).toBe('hello');
expect(result.data).toBe(undefined);
});
it('serialized httpResponse correctly with body buffer JSON Object', () => {
const json = '{"foo":"bar"}';
const httpResponse = new HTTPResponse({}, new Buffer(json));
const serialized = JSON.stringify(httpResponse);
const result = JSON.parse(serialized);
expect(result.text).toEqual('{"foo":"bar"}');
expect(result.data).toEqual({foo: 'bar'});
});
it('serialized httpResponse with Parse._encode should be allright', () => {
const json = '{"foo":"bar"}';
const httpResponse = new HTTPResponse({}, new Buffer(json));
const encoded = Parse._encode(httpResponse);
let foundData, foundText, foundBody = false;
for(var key in encoded) {
if (key == 'data') {
foundData = true;
}
if (key == 'text') {
foundText = true;
}
if (key == 'body') {
foundBody = true;
}
}
expect(foundData).toBe(true);
expect(foundText).toBe(true);
expect(foundBody).toBe(false);
});
});
+74
View File
@@ -0,0 +1,74 @@
const InMemoryCache = require('../src/Adapters/Cache/InMemoryCache').default;
describe('InMemoryCache', function() {
var BASE_TTL = {
ttl: 10
};
var NO_EXPIRE_TTL = {
ttl: NaN
};
var KEY = 'hello';
var KEY_2 = KEY + '_2';
var VALUE = 'world';
function wait(sleep) {
return new Promise(function(resolve) {
setTimeout(resolve, sleep);
})
}
it('should destroy a expire items in the cache', (done) => {
var cache = new InMemoryCache(BASE_TTL);
cache.put(KEY, VALUE);
var value = cache.get(KEY);
expect(value).toEqual(VALUE);
wait(BASE_TTL.ttl * 5).then(() => {
value = cache.get(KEY)
expect(value).toEqual(null);
done();
});
});
it('should delete items', (done) => {
var cache = new InMemoryCache(NO_EXPIRE_TTL);
cache.put(KEY, VALUE);
cache.put(KEY_2, VALUE);
expect(cache.get(KEY)).toEqual(VALUE);
expect(cache.get(KEY_2)).toEqual(VALUE);
cache.del(KEY);
expect(cache.get(KEY)).toEqual(null);
expect(cache.get(KEY_2)).toEqual(VALUE);
cache.del(KEY_2);
expect(cache.get(KEY)).toEqual(null);
expect(cache.get(KEY_2)).toEqual(null);
done();
});
it('should clear all items', (done) => {
var cache = new InMemoryCache(NO_EXPIRE_TTL);
cache.put(KEY, VALUE);
cache.put(KEY_2, VALUE);
expect(cache.get(KEY)).toEqual(VALUE);
expect(cache.get(KEY_2)).toEqual(VALUE);
cache.clear();
expect(cache.get(KEY)).toEqual(null);
expect(cache.get(KEY_2)).toEqual(null);
done();
});
it('should deafult TTL to 5 seconds', () => {
var cache = new InMemoryCache({});
expect(cache.ttl).toEqual(5 * 1000);
});
});
+57
View File
@@ -0,0 +1,57 @@
var InMemoryCacheAdapter = require('../src/Adapters/Cache/InMemoryCacheAdapter').default;
describe('InMemoryCacheAdapter', function() {
var KEY = 'hello';
var VALUE = 'world';
function wait(sleep) {
return new Promise(function(resolve) {
setTimeout(resolve, sleep);
})
}
it('should expose promisifyed methods', (done) => {
var cache = new InMemoryCacheAdapter({
ttl: NaN
});
// Verify all methods return promises.
Promise.all([
cache.put(KEY, VALUE),
cache.del(KEY),
cache.get(KEY),
cache.clear()
]).then(() => {
done();
});
});
it('should get/set/clear', (done) => {
var cache = new InMemoryCacheAdapter({
ttl: NaN
});
cache.put(KEY, VALUE)
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(VALUE))
.then(() => cache.clear())
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(null))
.then(done);
});
it('should expire after ttl', (done) => {
var cache = new InMemoryCacheAdapter({
ttl: 10
});
cache.put(KEY, VALUE)
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(VALUE))
.then(wait.bind(null, 50))
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(null))
.then(done);
})
});
+193
View File
@@ -0,0 +1,193 @@
var auth = require('../src/Auth');
var Config = require('../src/Config');
var rest = require('../src/rest');
var InstallationsRouter = require('../src/Routers/InstallationsRouter').InstallationsRouter;
describe('InstallationsRouter', () => {
it('uses find condition from request.body', (done) => {
var config = new Config('test');
var androidDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
'deviceType': 'android'
};
var iosDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
'deviceType': 'ios'
};
var request = {
config: config,
auth: auth.master(config),
body: {
where: {
deviceType: 'android'
}
},
query: {},
info: {}
};
var router = new InstallationsRouter();
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
.then(() => {
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
}).then(() => {
return router.handleFind(request);
}).then((res) => {
var results = res.response.results;
expect(results.length).toEqual(1);
done();
}).catch((err) => {
fail(JSON.stringify(err));
done();
});
});
it('uses find condition from request.query', (done) => {
var config = new Config('test');
var androidDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
'deviceType': 'android'
};
var iosDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
'deviceType': 'ios'
};
var request = {
config: config,
auth: auth.master(config),
body: {},
query: {
where: {
deviceType: 'android'
}
},
info: {}
};
var router = new InstallationsRouter();
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
.then(() => {
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
}).then(() => {
return router.handleFind(request);
}).then((res) => {
var results = res.response.results;
expect(results.length).toEqual(1);
done();
}).catch((err) => {
jfail(err);
done();
});
});
it('query installations with limit = 0', (done) => {
var config = new Config('test');
var androidDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
'deviceType': 'android'
};
var iosDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
'deviceType': 'ios'
};
var request = {
config: config,
auth: auth.master(config),
body: {},
query: {
limit: 0
},
info: {}
};
new Config('test');
var router = new InstallationsRouter();
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
.then(() => {
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
}).then(() => {
return router.handleFind(request);
}).then((res) => {
var response = res.response;
expect(response.results.length).toEqual(0);
done();
}).catch((err) => {
fail(JSON.stringify(err));
done();
});
});
it('query installations with count = 1', done => {
var config = new Config('test');
var androidDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
'deviceType': 'android'
};
var iosDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
'deviceType': 'ios'
};
var request = {
config: config,
auth: auth.master(config),
body: {},
query: {
count: 1
},
info: {}
};
var router = new InstallationsRouter();
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
.then(() => rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest))
.then(() => router.handleFind(request))
.then((res) => {
var response = res.response;
expect(response.results.length).toEqual(2);
expect(response.count).toEqual(2);
done();
})
.catch(error => {
fail(JSON.stringify(error));
done();
})
});
it('query installations with limit = 0 and count = 1', (done) => {
var config = new Config('test');
var androidDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
'deviceType': 'android'
};
var iosDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
'deviceType': 'ios'
};
var request = {
config: config,
auth: auth.master(config),
body: {},
query: {
limit: 0,
count: 1
},
info: {}
};
var router = new InstallationsRouter();
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
.then(() => {
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
}).then(() => {
return router.handleFind(request);
}).then((res) => {
var response = res.response;
expect(response.results.length).toEqual(0);
expect(response.count).toEqual(2);
done();
}).catch((err) => {
fail(JSON.stringify(err));
done();
});
});
});
+63
View File
@@ -0,0 +1,63 @@
var logging = require('../src/Adapters/Logger/WinstonLogger');
var winston = require('winston');
class TestTransport extends winston.Transport {
log(level, msg, meta, callback) {
callback(null, true);
}
}
describe('Logger', () => {
it('should add transport', () => {
const testTransport = new (TestTransport)({
name: 'test'
});
spyOn(testTransport, 'log');
logging.addTransport(testTransport);
expect(Object.keys(logging.logger.transports).length).toBe(4);
logging.logger.info('hi');
expect(testTransport.log).toHaveBeenCalled();
logging.removeTransport(testTransport);
expect(Object.keys(logging.logger.transports).length).toBe(3);
});
it('should have files transports', (done) => {
reconfigureServer().then(() => {
const transports = logging.logger.transports;
const transportKeys = Object.keys(transports);
expect(transportKeys.length).toBe(3);
done();
});
});
it('should disable files logs', (done) => {
reconfigureServer({
logsFolder: null
}).then(() => {
const transports = logging.logger.transports;
const transportKeys = Object.keys(transports);
expect(transportKeys.length).toBe(1);
done();
});
});
it('should enable JSON logs', (done) => {
// Force console transport
reconfigureServer({
logsFolder: null,
jsonLogs: true,
silent: false
}).then(() => {
spyOn(process.stdout, 'write');
logging.logger.info('hi', {key: 'value'});
expect(process.stdout.write).toHaveBeenCalled();
var firstLog = process.stdout.write.calls.first().args[0];
expect(firstLog).toEqual(JSON.stringify({key: 'value', level: 'info', message: 'hi' }) + '\n');
return reconfigureServer({
jsonLogs: false
});
}).then(() => {
done();
});
});
});
+11 -12
View File
@@ -1,20 +1,19 @@
var LoggerController = require('../src/Controllers/LoggerController').LoggerController;
var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter;
var WinstonLoggerAdapter = require('../src/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter;
describe('LoggerController', () => {
it('can check process a query witout throwing', (done) => {
it('can check process a query without throwing', (done) => {
// Make mock request
var query = {};
var loggerController = new LoggerController(new FileLoggerAdapter());
var loggerController = new LoggerController(new WinstonLoggerAdapter());
expect(() => {
loggerController.getLogs(query).then(function(res) {
expect(res.length).not.toBe(0);
done();
}).catch((err) => {
console.error(err);
fail("should not fail");
}).catch((err) => {
jfail(err);
done();
})
}).not.toThrow();
@@ -38,7 +37,7 @@ describe('LoggerController', () => {
done();
});
it('can process a query witout throwing', (done) => {
it('can process a query without throwing', (done) => {
// Make mock request
var query = {
from: "2016-01-01Z00:00:00",
@@ -59,7 +58,7 @@ describe('LoggerController', () => {
done();
});
it('can check process a query witout throwing', (done) => {
it('can check process a query without throwing', (done) => {
// Make mock request
var query = {
from: "2016-01-01",
@@ -69,14 +68,14 @@ describe('LoggerController', () => {
level: 'error'
};
var loggerController = new LoggerController(new FileLoggerAdapter());
var loggerController = new LoggerController(new WinstonLoggerAdapter());
expect(() => {
loggerController.getLogs(query).then(function(res) {
expect(res.length).toBe(0);
done();
}).catch((err) => {
console.error(err);
}).catch((err) => {
jfail(err);
fail("should not fail");
done();
})
@@ -85,7 +84,7 @@ describe('LoggerController', () => {
it('should throw without an adapter', (done) => {
expect(() => {
var loggerController = new LoggerController();
new LoggerController();
}).toThrow();
done();
});
+2 -2
View File
@@ -3,9 +3,9 @@
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;
var WinstonLoggerAdapter = require('../src/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter;
const loggerController = new LoggerController(new FileLoggerAdapter());
const loggerController = new LoggerController(new WinstonLoggerAdapter());
describe('LogsRouter', () => {
it('can check valid master key of request', (done) => {
+136
View File
@@ -0,0 +1,136 @@
var middlewares = require('../src/middlewares');
var AppCache = require('../src/cache').AppCache;
describe('middlewares', () => {
var fakeReq, fakeRes;
beforeEach(() => {
fakeReq = {
originalUrl: 'http://example.com/parse/',
url: 'http://example.com/',
body: {
_ApplicationId: 'FakeAppId'
},
headers: {},
get: (key) => {
return fakeReq.headers[key.toLowerCase()]
}
};
fakeRes = jasmine.createSpyObj('fakeRes', ['end', 'status']);
AppCache.put(fakeReq.body._ApplicationId, {});
});
afterEach(() => {
AppCache.del(fakeReq.body._ApplicationId);
});
it('should use _ContentType if provided', (done) => {
expect(fakeReq.headers['content-type']).toEqual(undefined);
var contentType = 'image/jpeg';
fakeReq.body._ContentType = contentType;
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeReq.headers['content-type']).toEqual(contentType);
expect(fakeReq.body._ContentType).toEqual(undefined);
done()
});
});
it('should give invalid response when keys are configured but no key supplied', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
restAPIKey: 'restAPIKey'
});
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
it('should give invalid response when keys are configured but supplied key is incorrect', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
restAPIKey: 'restAPIKey'
});
fakeReq.headers['x-parse-rest-api-key'] = 'wrongKey';
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
it('should give invalid response when keys are configured but different key is supplied', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
restAPIKey: 'restAPIKey'
});
fakeReq.headers['x-parse-client-key'] = 'clientKey';
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
it('should succeed when any one of the configured keys supplied', (done) => {
AppCache.put(fakeReq.body._ApplicationId, {
clientKey: 'clientKey',
masterKey: 'masterKey',
restAPIKey: 'restAPIKey'
});
fakeReq.headers['x-parse-rest-api-key'] = 'restAPIKey';
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});
it('should succeed when client key supplied but empty', (done) => {
AppCache.put(fakeReq.body._ApplicationId, {
clientKey: '',
masterKey: 'masterKey',
restAPIKey: 'restAPIKey'
});
fakeReq.headers['x-parse-client-key'] = '';
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});
it('should succeed when no keys are configured and none supplied', (done) => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey'
});
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});
const BodyParams = {
clientVersion: '_ClientVersion',
installationId: '_InstallationId',
sessionToken: '_SessionToken',
masterKey: '_MasterKey',
javascriptKey: '_JavaScriptKey'
};
const BodyKeys = Object.keys(BodyParams);
BodyKeys.forEach((infoKey) => {
const bodyKey = BodyParams[infoKey];
const keyValue = 'Fake' + bodyKey;
// javascriptKey is the only one that gets defaulted,
const otherKeys = BodyKeys.filter((otherKey) => otherKey !== infoKey && otherKey !== 'javascriptKey');
it(`it should pull ${bodyKey} into req.info`, (done) => {
fakeReq.body[bodyKey] = keyValue;
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeReq.body[bodyKey]).toEqual(undefined);
expect(fakeReq.info[infoKey]).toEqual(keyValue);
otherKeys.forEach((otherKey) => {
expect(fakeReq.info[otherKey]).toEqual(undefined);
});
done();
});
});
});
});
+17 -6
View File
@@ -1,10 +1,21 @@
module.exports = options => {
if (!options) {
throw "Options were not provided"
}
return {
sendVerificationEmail: () => Promise.resolve(),
if (!options) {
throw "Options were not provided"
}
const adapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve()
}
};
if (options.sendMail) {
adapter.sendMail = options.sendMail
}
if (options.sendPasswordResetEmail) {
adapter.sendPasswordResetEmail = options.sendPasswordResetEmail
}
if (options.sendVerificationEmail) {
adapter.sendVerificationEmail = options.sendVerificationEmail;
}
return adapter;
}
+9
View File
@@ -0,0 +1,9 @@
module.exports = function(options) {
return {
options: options,
send: function() {},
getValidPushTypes: function() {
return Object.keys(options.options);
}
};
};
+73
View File
@@ -0,0 +1,73 @@
'use strict';
const MongoSchemaCollection = require('../src/Adapters/Storage/Mongo/MongoSchemaCollection').default;
describe('MongoSchemaCollection', () => {
it('can transform legacy _client_permissions keys to parse format', done => {
expect(MongoSchemaCollection._TESTmongoSchemaToParseSchema({
"_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",
"pushType":"string",
"GCMSenderId":"string",
"timeZone":"string",
"localeIdentifier":"string",
"badge":"number",
"appVersion":"string",
"appName":"string",
"appIdentifier":"string",
"parseVersion":"string",
})).toEqual({
className: '_Installation',
fields: {
installationId: { type: 'String' },
deviceToken: { type: 'String' },
deviceType: { type: 'String' },
channels: { type: 'Array' },
user: { type: 'Pointer', targetClass: '_User' },
pushType: { type: 'String' },
GCMSenderId: { type: 'String' },
timeZone: { type: 'String' },
localeIdentifier: { type: 'String' },
badge: { type: 'Number' },
appVersion: { type: 'String' },
appName: { type: 'String' },
appIdentifier: { type: 'String' },
parseVersion: { type: 'String' },
ACL: { type: 'ACL' },
createdAt: { type: 'Date' },
updatedAt: { type: 'Date' },
objectId: { type: 'String' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
}
});
done();
});
});
+210 -7
View File
@@ -2,12 +2,22 @@
const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
const MongoClient = require('mongodb').MongoClient;
const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
// These tests are specific to the mongo storage adapter + mongo storage format
// and will eventually be moved into their own repo
describe_only_db('mongo')('MongoStorageAdapter', () => {
beforeEach(done => {
new MongoStorageAdapter({ uri: databaseURI })
.deleteAllClasses()
.then(done, fail);
});
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();
new MongoStorageAdapter({
uri: '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)
@@ -16,8 +26,9 @@ describe('MongoStorageAdapter', () => {
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();
new MongoStorageAdapter({
uri: '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)
@@ -27,11 +38,203 @@ describe('MongoStorageAdapter', () => {
// 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();
new MongoStorageAdapter({
uri: '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)
);
});
it('stores objectId in _id', done => {
const adapter = new MongoStorageAdapter({ uri: databaseURI });
adapter.createObject('Foo', { fields: {} }, { objectId: 'abcde' })
.then(() => adapter._rawFind('Foo', {}))
.then(results => {
expect(results.length).toEqual(1);
var obj = results[0];
expect(obj._id).toEqual('abcde');
expect(obj.objectId).toBeUndefined();
done();
});
});
it('find succeeds when query is within maxTimeMS', (done) => {
const maxTimeMS = 250;
const adapter = new MongoStorageAdapter({
uri: databaseURI,
mongoOptions: { maxTimeMS },
});
adapter.createObject('Foo', { fields: {} }, { objectId: 'abcde' })
.then(() => adapter._rawFind('Foo', { '$where': `sleep(${maxTimeMS / 2})` }))
.then(
() => done(),
(err) => {
done.fail(`maxTimeMS should not affect fast queries ${err}`);
}
);
})
it('find fails when query exceeds maxTimeMS', (done) => {
const maxTimeMS = 250;
const adapter = new MongoStorageAdapter({
uri: databaseURI,
mongoOptions: { maxTimeMS },
});
adapter.createObject('Foo', { fields: {} }, { objectId: 'abcde' })
.then(() => adapter._rawFind('Foo', { '$where': `sleep(${maxTimeMS * 2})` }))
.then(
() => {
done.fail('Find succeeded despite taking too long!');
},
(err) => {
expect(err.name).toEqual('MongoError');
expect(err.code).toEqual(50);
expect(err.message).toEqual('operation exceeded time limit');
done();
}
);
});
it('stores pointers with a _p_ prefix', (done) => {
const obj = {
objectId: 'bar',
aPointer: {
__type: 'Pointer',
className: 'JustThePointer',
objectId: 'qwerty'
}
};
const adapter = new MongoStorageAdapter({ uri: databaseURI });
adapter.createObject('APointerDarkly', { fields: {
objectId: { type: 'String' },
aPointer: { type: 'Pointer', targetClass: 'JustThePointer' },
}}, obj)
.then(() => adapter._rawFind('APointerDarkly', {}))
.then(results => {
expect(results.length).toEqual(1);
const output = results[0];
expect(typeof output._id).toEqual('string');
expect(typeof output._p_aPointer).toEqual('string');
expect(output._p_aPointer).toEqual('JustThePointer$qwerty');
expect(output.aPointer).toBeUndefined();
done();
});
});
it('handles object and subdocument', done => {
const adapter = new MongoStorageAdapter({ uri: databaseURI });
const schema = { fields : { subdoc: { type: 'Object' } } };
const obj = { subdoc: {foo: 'bar', wu: 'tan'} };
adapter.createObject('MyClass', schema, obj)
.then(() => adapter._rawFind('MyClass', {}))
.then(results => {
expect(results.length).toEqual(1);
const mob = results[0];
expect(typeof mob.subdoc).toBe('object');
expect(mob.subdoc.foo).toBe('bar');
expect(mob.subdoc.wu).toBe('tan');
const obj = { 'subdoc.wu': 'clan' };
return adapter.findOneAndUpdate('MyClass', schema, {}, obj);
})
.then(() => adapter._rawFind('MyClass', {}))
.then(results => {
expect(results.length).toEqual(1);
const mob = results[0];
expect(typeof mob.subdoc).toBe('object');
expect(mob.subdoc.foo).toBe('bar');
expect(mob.subdoc.wu).toBe('clan');
done();
});
});
it('handles creating an array, object, date', (done) => {
const adapter = new MongoStorageAdapter({ uri: databaseURI });
const obj = {
array: [1, 2, 3],
object: {foo: 'bar'},
date: {
__type: 'Date',
iso: '2016-05-26T20:55:01.154Z',
},
};
const schema = { fields: {
array: { type: 'Array' },
object: { type: 'Object' },
date: { type: 'Date' },
} };
adapter.createObject('MyClass', schema, obj)
.then(() => adapter._rawFind('MyClass', {}))
.then(results => {
expect(results.length).toEqual(1);
const mob = results[0];
expect(mob.array instanceof Array).toBe(true);
expect(typeof mob.object).toBe('object');
expect(mob.date instanceof Date).toBe(true);
return adapter.find('MyClass', schema, {}, {});
})
.then(results => {
expect(results.length).toEqual(1);
const mob = results[0];
expect(mob.array instanceof Array).toBe(true);
expect(typeof mob.object).toBe('object');
expect(mob.date.__type).toBe('Date');
expect(mob.date.iso).toBe('2016-05-26T20:55:01.154Z');
done();
})
.catch(error => {
console.log(error);
fail();
done();
});
});
it("handles updating a single object with array, object date", (done) => {
const adapter = new MongoStorageAdapter({ uri: databaseURI });
const schema = { fields: {
array: { type: 'Array' },
object: { type: 'Object' },
date: { type: 'Date' },
} };
adapter.createObject('MyClass', schema, {})
.then(() => adapter._rawFind('MyClass', {}))
.then(results => {
expect(results.length).toEqual(1);
const update = {
array: [1, 2, 3],
object: {foo: 'bar'},
date: {
__type: 'Date',
iso: '2016-05-26T20:55:01.154Z',
},
};
const query = {};
return adapter.findOneAndUpdate('MyClass', schema, query, update)
})
.then(results => {
const mob = results;
expect(mob.array instanceof Array).toBe(true);
expect(typeof mob.object).toBe('object');
expect(mob.date.__type).toBe('Date');
expect(mob.date.iso).toBe('2016-05-26T20:55:01.154Z');
return adapter._rawFind('MyClass', {});
})
.then(results => {
expect(results.length).toEqual(1);
const mob = results[0];
expect(mob.array instanceof Array).toBe(true);
expect(typeof mob.object).toBe('object');
expect(mob.date instanceof Date).toBe(true);
done();
})
.catch(error => {
console.log(error);
fail();
done();
});
});
});
+337
View File
@@ -0,0 +1,337 @@
// These tests are unit tests designed to only test transform.js.
"use strict";
const transform = require('../src/Adapters/Storage/Mongo/MongoTransform');
const dd = require('deep-diff');
const mongodb = require('mongodb');
describe('parseObjectToMongoObjectForCreate', () => {
it('a basic number', (done) => {
var input = {five: 5};
var output = transform.parseObjectToMongoObjectForCreate(null, input, {
fields: {five: {type: 'Number'}}
});
jequal(input, output);
done();
});
it('an object with null values', (done) => {
var input = {objectWithNullValues: {isNull: null, notNull: 3}};
var output = transform.parseObjectToMongoObjectForCreate(null, input, {
fields: {objectWithNullValues: {type: 'object'}}
});
jequal(input, output);
done();
});
it('built-in timestamps', (done) => {
var input = {
createdAt: "2015-10-06T21:24:50.332Z",
updatedAt: "2015-10-06T21:24:50.332Z"
};
var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} });
expect(output._created_at instanceof Date).toBe(true);
expect(output._updated_at instanceof Date).toBe(true);
done();
});
it('array of pointers', (done) => {
var pointer = {
__type: 'Pointer',
objectId: 'myId',
className: 'Blah',
};
var out = transform.parseObjectToMongoObjectForCreate(null, {pointers: [pointer]},{
fields: {pointers: {type: 'Array'}}
});
jequal([pointer], out.pointers);
done();
});
//TODO: object creation requests shouldn't be seeing __op delete, it makes no sense to
//have __op delete in a new object. Figure out what this should actually be testing.
xit('a delete op', (done) => {
var input = {deleteMe: {__op: 'Delete'}};
var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} });
jequal(output, {});
done();
});
it('Doesnt allow ACL, as Parse Server should tranform ACL to _wperm + _rperm', done => {
var input = {ACL: {'0123': {'read': true, 'write': true}}};
expect(() => transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} })).toThrow();
done();
});
it('plain', (done) => {
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
var out = transform.parseObjectToMongoObjectForCreate(null, {location: geoPoint},{
fields: {location: {type: 'GeoPoint'}}
});
expect(out.location).toEqual([180, -180]);
done();
});
it('in array', (done) => {
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
var out = transform.parseObjectToMongoObjectForCreate(null, {locations: [geoPoint, geoPoint]},{
fields: {locations: {type: 'Array'}}
});
expect(out.locations).toEqual([geoPoint, geoPoint]);
done();
});
it('in sub-object', (done) => {
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
var out = transform.parseObjectToMongoObjectForCreate(null, { locations: { start: geoPoint }},{
fields: {locations: {type: 'Object'}}
});
expect(out).toEqual({ locations: { start: geoPoint } });
done();
});
it('objectId', (done) => {
var out = transform.transformWhere(null, {objectId: 'foo'});
expect(out._id).toEqual('foo');
done();
});
it('objectId in a list', (done) => {
var input = {
objectId: {'$in': ['one', 'two', 'three']},
};
var output = transform.transformWhere(null, input);
jequal(input.objectId, output._id);
done();
});
it('built-in timestamps', (done) => {
var input = {createdAt: new Date(), updatedAt: new Date()};
var output = transform.mongoObjectToParseObject(null, input, { fields: {} });
expect(typeof output.createdAt).toEqual('string');
expect(typeof output.updatedAt).toEqual('string');
done();
});
it('pointer', (done) => {
var input = {_p_userPointer: '_User$123'};
var output = transform.mongoObjectToParseObject(null, input, {
fields: { userPointer: { type: 'Pointer', targetClass: '_User' } },
});
expect(typeof output.userPointer).toEqual('object');
expect(output.userPointer).toEqual(
{__type: 'Pointer', className: '_User', objectId: '123'}
);
done();
});
it('null pointer', (done) => {
var input = {_p_userPointer: null};
var output = transform.mongoObjectToParseObject(null, input, {
fields: { userPointer: { type: 'Pointer', targetClass: '_User' } },
});
expect(output.userPointer).toBeUndefined();
done();
});
it('file', (done) => {
var input = {picture: 'pic.jpg'};
var output = transform.mongoObjectToParseObject(null, input, {
fields: { picture: { type: 'File' }},
});
expect(typeof output.picture).toEqual('object');
expect(output.picture).toEqual({__type: 'File', name: 'pic.jpg'});
done();
});
it('geopoint', (done) => {
var input = {location: [180, -180]};
var output = transform.mongoObjectToParseObject(null, input, {
fields: { location: { type: 'GeoPoint' }},
});
expect(typeof output.location).toEqual('object');
expect(output.location).toEqual(
{__type: 'GeoPoint', longitude: 180, latitude: -180}
);
done();
});
it('bytes', (done) => {
var input = {binaryData: "aGVsbG8gd29ybGQ="};
var output = transform.mongoObjectToParseObject(null, input, {
fields: { binaryData: { type: 'Bytes' }},
});
expect(typeof output.binaryData).toEqual('object');
expect(output.binaryData).toEqual(
{__type: 'Bytes', base64: "aGVsbG8gd29ybGQ="}
);
done();
});
it('nested array', (done) => {
var input = {arr: [{_testKey: 'testValue' }]};
var output = transform.mongoObjectToParseObject(null, input, {
fields: { arr: { type: 'Array' } },
});
expect(Array.isArray(output.arr)).toEqual(true);
expect(output.arr).toEqual([{ _testKey: 'testValue'}]);
done();
});
it('untransforms objects containing nested special keys', done => {
const input = {array: [{
_id: "Test ID",
_hashed_password: "I Don't know why you would name a key this, but if you do it should work",
_tombstone: {
_updated_at: "I'm sure people will nest keys like this",
_acl: 7,
_id: { someString: "str", someNumber: 7},
regularKey: { moreContents: [1, 2, 3] },
},
regularKey: "some data",
}]}
const output = transform.mongoObjectToParseObject(null, input, {
fields: { array: { type: 'Array' }},
});
expect(dd(output, input)).toEqual(undefined);
done();
});
it('changes new pointer key', (done) => {
var input = {
somePointer: {__type: 'Pointer', className: 'Micro', objectId: 'oft'}
};
var output = transform.parseObjectToMongoObjectForCreate(null, input, {
fields: {somePointer: {type: 'Pointer'}}
});
expect(typeof output._p_somePointer).toEqual('string');
expect(output._p_somePointer).toEqual('Micro$oft');
done();
});
it('changes existing pointer keys', (done) => {
var input = {
userPointer: {__type: 'Pointer', className: '_User', objectId: 'qwerty'}
};
var output = transform.parseObjectToMongoObjectForCreate(null, input, {
fields: {userPointer: {type: 'Pointer'}}
});
expect(typeof output._p_userPointer).toEqual('string');
expect(output._p_userPointer).toEqual('_User$qwerty');
done();
});
it('writes the old ACL format in addition to rperm and wperm on create', (done) => {
var input = {
_rperm: ['*'],
_wperm: ['Kevin'],
};
var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} });
expect(typeof output._acl).toEqual('object');
expect(output._acl['Kevin'].w).toBeTruthy();
expect(output._acl['Kevin'].r).toBeUndefined();
expect(output._rperm).toEqual(input._rperm);
expect(output._wperm).toEqual(input._wperm);
done();
});
it('removes Relation types', (done) => {
var input = {
aRelation: { __type: 'Relation', className: 'Stuff' },
};
var output = transform.parseObjectToMongoObjectForCreate(null, input, {
fields: {
aRelation: { __type: 'Relation', className: 'Stuff' },
},
});
expect(output).toEqual({});
done();
});
it('writes the old ACL format in addition to rperm and wperm on update', (done) => {
var input = {
_rperm: ['*'],
_wperm: ['Kevin']
};
var output = transform.transformUpdate(null, input, { fields: {} });
var set = output.$set;
expect(typeof set).toEqual('object');
expect(typeof set._acl).toEqual('object');
expect(set._acl['Kevin'].w).toBeTruthy();
expect(set._acl['Kevin'].r).toBeUndefined();
expect(set._rperm).toEqual(input._rperm);
expect(set._wperm).toEqual(input._wperm);
done();
});
it('untransforms from _rperm and _wperm to ACL', (done) => {
var input = {
_rperm: ["*"],
_wperm: ["Kevin"]
};
var output = transform.mongoObjectToParseObject(null, input, { fields: {} });
expect(output._rperm).toEqual(['*']);
expect(output._wperm).toEqual(['Kevin']);
expect(output.ACL).toBeUndefined()
done();
});
it('untransforms mongodb number types', (done) => {
var input = {
long: mongodb.Long.fromNumber(Number.MAX_SAFE_INTEGER),
double: new mongodb.Double(Number.MAX_VALUE)
}
var output = transform.mongoObjectToParseObject(null, input, {
fields: {
long: { type: 'Number' },
double: { type: 'Number' },
},
});
expect(output.long).toBe(Number.MAX_SAFE_INTEGER);
expect(output.double).toBe(Number.MAX_VALUE);
done();
});
it('Date object where iso attribute is of type Date', (done) => {
var input = {
ts : { __type: 'Date', iso: new Date('2017-01-18T00:00:00.000Z') }
}
var output = transform.mongoObjectToParseObject(null, input, {
fields : {
ts : { type : 'Date' }
}
});
expect(output.ts.iso).toEqual('2017-01-18T00:00:00.000Z');
done();
});
it('Date object where iso attribute is of type String', (done) => {
var input = {
ts : { __type: 'Date', iso: '2017-01-18T00:00:00.000Z' }
}
var output = transform.mongoObjectToParseObject(null, input, {
fields : {
ts : { type : 'Date' }
}
});
expect(output.ts.iso).toEqual('2017-01-18T00:00:00.000Z');
done();
});
});
describe('transformUpdate', () => {
it('removes Relation types', (done) => {
var input = {
aRelation: { __type: 'Relation', className: 'Stuff' },
};
var output = transform.transformUpdate(null, input, {
fields: {
aRelation: { __type: 'Relation', className: 'Stuff' },
},
});
expect(output).toEqual({});
done();
});
});
+37
View File
@@ -0,0 +1,37 @@
var NullCacheAdapter = require('../src/Adapters/Cache/NullCacheAdapter').default;
describe('NullCacheAdapter', function() {
var KEY = 'hello';
var VALUE = 'world';
it('should expose promisifyed methods', (done) => {
var cache = new NullCacheAdapter({
ttl: NaN
});
// Verify all methods return promises.
Promise.all([
cache.put(KEY, VALUE),
cache.del(KEY),
cache.get(KEY),
cache.clear()
]).then(() => {
done();
});
});
it('should get/set/clear', (done) => {
var cache = new NullCacheAdapter({
ttl: NaN
});
cache.put(KEY, VALUE)
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(null))
.then(() => cache.clear())
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(null))
.then(done);
});
});
-326
View File
@@ -1,326 +0,0 @@
var OAuth = require("../src/authDataManager/OAuth1Client");
var request = require('request');
var Config = require("../src/Config");
describe('OAuth', function() {
it("Nonce should have right length", (done) => {
jequal(OAuth.nonce().length, 30);
done();
});
it("Should properly build parameter string", (done) => {
var string = OAuth.buildParameterString({c:1, a:2, b:3})
jequal(string, "a=2&b=3&c=1");
done();
});
it("Should properly build empty parameter string", (done) => {
var string = OAuth.buildParameterString()
jequal(string, "");
done();
});
it("Should properly build signature string", (done) => {
var string = OAuth.buildSignatureString("get", "http://dummy.com", "");
jequal(string, "GET&http%3A%2F%2Fdummy.com&");
done();
});
it("Should properly generate request signature", (done) => {
var request = {
host: "dummy.com",
path: "path"
};
var oauth_params = {
oauth_timestamp: 123450000,
oauth_nonce: "AAAAAAAAAAAAAAAAA",
oauth_consumer_key: "hello",
oauth_token: "token"
};
var consumer_secret = "world";
var auth_token_secret = "secret";
request = OAuth.signRequest(request, oauth_params, consumer_secret, auth_token_secret);
jequal(request.headers['Authorization'], 'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="8K95bpQcDi9Nd2GkhumTVcw4%2BXw%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"');
done();
});
it("Should properly build request", (done) => {
var options = {
host: "dummy.com",
consumer_key: "hello",
consumer_secret: "world",
auth_token: "token",
auth_token_secret: "secret",
// Custom oauth params for tests
oauth_params: {
oauth_timestamp: 123450000,
oauth_nonce: "AAAAAAAAAAAAAAAAA"
}
};
var path = "path";
var method = "get";
var oauthClient = new OAuth(options);
var req = oauthClient.buildRequest(method, path, {"query": "param"});
jequal(req.host, options.host);
jequal(req.path, "/"+path+"?query=param");
jequal(req.method, "GET");
jequal(req.headers['Content-Type'], 'application/x-www-form-urlencoded');
jequal(req.headers['Authorization'], 'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="wNkyEkDE%2F0JZ2idmqyrgHdvC0rs%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"')
done();
});
function validateCannotAuthenticateError(data, done) {
jequal(typeof data, "object");
jequal(typeof data.errors, "object");
var errors = data.errors;
jequal(typeof errors[0], "object");
// Cannot authenticate error
jequal(errors[0].code, 32);
done();
}
it("Should fail a GET request", (done) => {
var options = {
host: "api.twitter.com",
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
};
var path = "/1.1/help/configuration.json";
var params = {"lang": "en"};
var oauthClient = new OAuth(options);
oauthClient.get(path, params).then(function(data){
validateCannotAuthenticateError(data, done);
})
});
it("Should fail a POST request", (done) => {
var options = {
host: "api.twitter.com",
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
};
var body = {
lang: "en"
};
var path = "/1.1/account/settings.json";
var oauthClient = new OAuth(options);
oauthClient.post(path, null, body).then(function(data){
validateCannotAuthenticateError(data, done);
})
});
it("Should fail a request", (done) => {
var options = {
host: "localhost",
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
};
var body = {
lang: "en"
};
var path = "/";
var oauthClient = new OAuth(options);
oauthClient.post(path, null, body).then(function(data){
jequal(false, true);
done();
}).catch(function(){
jequal(true, true);
done();
})
});
["facebook", "github", "instagram", "google", "linkedin", "meetup", "twitter"].map(function(providerName){
it("Should validate structure of "+providerName, (done) => {
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);
done();
});
});
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;
}
});
var createOAuthUser = function(callback) {
var jsonBody = {
authData: {
myoauth: getMockMyOauthProvider().authData
}
};
var options = {
headers: {'X-Parse-Application-Id': 'test',
'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)
};
return request.post(options, callback);
}
it("should create user with REST API", (done) => {
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();
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();
})
});
});
it("should only create a single user with REST API", (done) => {
var objectId;
createOAuthUser((error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(b.objectId).not.toBeNull();
expect(b.objectId).not.toBeUndefined();
objectId = b.objectId;
createOAuthUser((error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(b.objectId).not.toBeNull();
expect(b.objectId).not.toBeUndefined();
expect(b.objectId).toBe(objectId);
done();
});
});
});
it("unlink and link with custom provider", (done) => {
var provider = getMockMyOauthProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("myoauth", {
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("myoauth"), "User should be linked to myoauth");
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();
}
});
});
},
error: function(model, error) {
ok(false, "unlinking should succeed");
done();
}
});
},
error: function(model, error) {
ok(false, "linking should have worked");
done();
}
});
});
})
+136
View File
@@ -0,0 +1,136 @@
var OAuth = require("../src/Adapters/Auth/OAuth1Client");
describe('OAuth', function() {
it("Nonce should have right length", (done) => {
jequal(OAuth.nonce().length, 30);
done();
});
it("Should properly build parameter string", (done) => {
var string = OAuth.buildParameterString({c:1, a:2, b:3})
jequal(string, "a=2&b=3&c=1");
done();
});
it("Should properly build empty parameter string", (done) => {
var string = OAuth.buildParameterString()
jequal(string, "");
done();
});
it("Should properly build signature string", (done) => {
var string = OAuth.buildSignatureString("get", "http://dummy.com", "");
jequal(string, "GET&http%3A%2F%2Fdummy.com&");
done();
});
it("Should properly generate request signature", (done) => {
var request = {
host: "dummy.com",
path: "path"
};
var oauth_params = {
oauth_timestamp: 123450000,
oauth_nonce: "AAAAAAAAAAAAAAAAA",
oauth_consumer_key: "hello",
oauth_token: "token"
};
var consumer_secret = "world";
var auth_token_secret = "secret";
request = OAuth.signRequest(request, oauth_params, consumer_secret, auth_token_secret);
jequal(request.headers['Authorization'], 'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="8K95bpQcDi9Nd2GkhumTVcw4%2BXw%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"');
done();
});
it("Should properly build request", (done) => {
var options = {
host: "dummy.com",
consumer_key: "hello",
consumer_secret: "world",
auth_token: "token",
auth_token_secret: "secret",
// Custom oauth params for tests
oauth_params: {
oauth_timestamp: 123450000,
oauth_nonce: "AAAAAAAAAAAAAAAAA"
}
};
var path = "path";
var method = "get";
var oauthClient = new OAuth(options);
var req = oauthClient.buildRequest(method, path, {"query": "param"});
jequal(req.host, options.host);
jequal(req.path, "/" + path + "?query=param");
jequal(req.method, "GET");
jequal(req.headers['Content-Type'], 'application/x-www-form-urlencoded');
jequal(req.headers['Authorization'], 'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="wNkyEkDE%2F0JZ2idmqyrgHdvC0rs%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"')
done();
});
function validateCannotAuthenticateError(data, done) {
jequal(typeof data, "object");
jequal(typeof data.errors, "object");
var errors = data.errors;
jequal(typeof errors[0], "object");
// Cannot authenticate error
jequal(errors[0].code, 32);
done();
}
it("Should fail a GET request", (done) => {
var options = {
host: "api.twitter.com",
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
};
var path = "/1.1/help/configuration.json";
var params = {"lang": "en"};
var oauthClient = new OAuth(options);
oauthClient.get(path, params).then(function(data){
validateCannotAuthenticateError(data, done);
})
});
it("Should fail a POST request", (done) => {
var options = {
host: "api.twitter.com",
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
};
var body = {
lang: "en"
};
var path = "/1.1/account/settings.json";
var oauthClient = new OAuth(options);
oauthClient.post(path, null, body).then(function(data){
validateCannotAuthenticateError(data, done);
})
});
it("Should fail a request", (done) => {
var options = {
host: "localhost",
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
};
var body = {
lang: "en"
};
var path = "/";
var oauthClient = new OAuth(options);
oauthClient.post(path, null, body).then(function(){
jequal(false, true);
done();
}).catch(function(){
jequal(true, true);
done();
})
});
});
+157 -45
View File
@@ -1,23 +1,33 @@
'use strict';
const request = require('request');
const delayPromise = (delay) => {
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
}
describe('Parse.Push', () => {
var setup = function() {
var sendToInstallationSpy = jasmine.createSpy();
var pushAdapter = {
send: function(body, installations) {
var badge = body.data.badge;
let promises = installations.map((installation) => {
const promises = installations.map((installation) => {
sendToInstallationSpy(installation);
if (installation.deviceType == "ios") {
expect(installation.badge).toEqual(badge);
expect(installation.originalBadge+1).toEqual(installation.badge);
expect(installation.originalBadge + 1).toEqual(installation.badge);
} else {
expect(installation.badge).toBeUndefined();
}
return Promise.resolve({
err: null,
deviceType: installation.deviceType,
result: true
device: installation,
transmitted: true
})
});
return Promise.all(promises);
@@ -27,65 +37,167 @@ describe('Parse.Push', () => {
}
}
setServerConfiguration({
return reconfigureServer({
appId: Parse.applicationId,
masterKey: Parse.masterKey,
serverURL: Parse.serverURL,
push: {
adapter: pushAdapter
}
});
})
.then(() => {
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);
})
.then(() => {
return {
sendToInstallationSpy,
};
})
.catch((err) => {
console.error(err);
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);
throw err;
})
}
it('should properly send push', (done) => {
return setup().then(() => {
return setup().then(({ sendToInstallationSpy }) => {
return Parse.Push.send({
where: {
deviceType: 'ios'
},
data: {
badge: 'Increment',
alert: 'Hello world!'
}
}, {useMasterKey: true})
})
.then(() => {
where: {
deviceType: 'ios'
},
data: {
badge: 'Increment',
alert: 'Hello world!'
}
}, {useMasterKey: true})
.then(() => {
return delayPromise(500);
})
.then(() => {
expect(sendToInstallationSpy.calls.count()).toEqual(10);
})
}).then(() => {
done();
}, (err) => {
console.error();
fail('should not fail sending push')
}).catch((err) => {
jfail(err);
done();
});
});
it('should properly send push with lowercaseIncrement', (done) => {
return setup().then(() => {
return setup().then(() => {
return Parse.Push.send({
where: {
deviceType: 'ios'
},
data: {
badge: 'increment',
alert: 'Hello world!'
}
}, {useMasterKey: true})
}).then(() => {
where: {
deviceType: 'ios'
},
data: {
badge: 'increment',
alert: 'Hello world!'
}
}, {useMasterKey: true})
}).then(() => {
return delayPromise(500);
}).then(() => {
done();
}, (err) => {
console.error();
fail('should not fail sending push')
}).catch((err) => {
jfail(err);
done();
});
});
it('should not allow clients to query _PushStatus', done => {
setup()
.then(() => Parse.Push.send({
where: {
deviceType: 'ios'
},
data: {
badge: 'increment',
alert: 'Hello world!'
}
}, {useMasterKey: true}))
.then(() => {
request.get({
url: 'http://localhost:8378/1/classes/_PushStatus',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
},
}, (error, response, body) => {
expect(body.error).toEqual('unauthorized');
done();
});
}).catch((err) => {
jfail(err);
done();
});
});
it('should allow master key to query _PushStatus', done => {
setup()
.then(() => Parse.Push.send({
where: {
deviceType: 'ios'
},
data: {
badge: 'increment',
alert: 'Hello world!'
}
}, {useMasterKey: true}))
.then(() => {
request.get({
url: 'http://localhost:8378/1/classes/_PushStatus',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
}, (error, response, body) => {
try {
expect(body.results.length).toEqual(1);
expect(body.results[0].query).toEqual('{"deviceType":"ios"}');
expect(body.results[0].payload).toEqual('{"badge":"increment","alert":"Hello world!"}');
} catch(e) {
jfail(e);
}
done();
});
}).catch((err) => {
jfail(err);
done();
});
});
it('should throw error if missing push configuration', done => {
reconfigureServer({push: null})
.then(() => {
return Parse.Push.send({
where: {
deviceType: 'ios'
},
data: {
badge: 'increment',
alert: 'Hello world!'
}
}, {useMasterKey: true})
}).then(() => {
fail('should not succeed');
}, (err) => {
expect(err.code).toEqual(Parse.Error.PUSH_MISCONFIGURED);
done();
}).catch((err) => {
jfail(err);
done();
});
});
+543 -437
View File
File diff suppressed because it is too large Load Diff
+891 -568
View File
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -2,7 +2,6 @@ var ParseCloudCodePublisher = require('../src/LiveQuery/ParseCloudCodePublisher'
var Parse = require('parse/node');
describe('ParseCloudCodePublisher', function() {
beforeEach(function(done) {
// Mock ParsePubSub
var mockParsePubSub = {
@@ -21,7 +20,7 @@ describe('ParseCloudCodePublisher', function() {
it('can initialize', function() {
var config = {}
var publisher = new ParseCloudCodePublisher(config);
new ParseCloudCodePublisher(config);
var ParsePubSub = require('../src/LiveQuery/ParsePubSub').ParsePubSub;
expect(ParsePubSub.createPublisher).toHaveBeenCalledWith(config);
@@ -33,7 +32,7 @@ describe('ParseCloudCodePublisher', function() {
var request = {};
publisher.onCloudCodeAfterSave(request);
expect(publisher._onCloudCodeMessage).toHaveBeenCalledWith('afterSave', request);
expect(publisher._onCloudCodeMessage).toHaveBeenCalledWith(Parse.applicationId + 'afterSave', request);
});
it('can handle cloud code afterDelete request', function() {
@@ -42,7 +41,7 @@ describe('ParseCloudCodePublisher', function() {
var request = {};
publisher.onCloudCodeAfterDelete(request);
expect(publisher._onCloudCodeMessage).toHaveBeenCalledWith('afterDelete', request);
expect(publisher._onCloudCodeMessage).toHaveBeenCalledWith(Parse.applicationId + 'afterDelete', request);
});
it('can handle cloud code request', function() {
+112 -19
View File
@@ -36,6 +36,35 @@ describe('Parse.File testing', () => {
});
});
it('works with _ContentType', done => {
request.post({
url: 'http://localhost:8378/1/files/file',
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
_ContentType: 'text/html',
base64: 'PGh0bWw+PC9odG1sPgo='
})
}, (error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(b.name).toMatch(/_file.html/);
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
request.get(b.url, (error, response, body) => {
try {
expect(response.headers['content-type']).toMatch('^text/html');
expect(error).toBe(null);
expect(body).toEqual('<html></html>\n');
} catch(e) {
jfail(e);
}
done();
});
});
});
it('works without Content-Type', done => {
var headers = {
'X-Parse-Application-Id': 'test',
@@ -84,7 +113,7 @@ describe('Parse.File testing', () => {
'X-Parse-Master-Key': 'test'
},
url: 'http://localhost:8378/1/files/' + b.name
}, (error, response, body) => {
}, (error, response) => {
expect(error).toBe(null);
expect(response.statusCode).toEqual(200);
request.get({
@@ -93,9 +122,13 @@ describe('Parse.File testing', () => {
'X-Parse-REST-API-Key': 'rest'
},
url: b.url
}, (error, response, body) => {
}, (error, response) => {
expect(error).toBe(null);
expect(response.statusCode).toEqual(404);
try {
expect(response.statusCode).toEqual(404);
} catch(e) {
jfail(e);
}
done();
});
});
@@ -182,7 +215,7 @@ describe('Parse.File testing', () => {
notEqual(file.name(), "hello.txt");
done();
}
}));
}, done));
});
it("save file in object", done => {
@@ -207,9 +240,9 @@ describe('Parse.File testing', () => {
}
}));
}
}));
}, done));
}
}));
}, done));
});
it("save file in object with escaped characters in filename", done => {
@@ -235,9 +268,9 @@ describe('Parse.File testing', () => {
}
}));
}
}));
}, done));
}
}));
}, done));
});
it("autosave file in object", done => {
@@ -257,9 +290,9 @@ describe('Parse.File testing', () => {
notEqual(file.name(), "hello.txt");
done();
}
}));
}, done));
}
}));
}, done));
});
it("autosave file in object in object", done => {
@@ -286,9 +319,9 @@ describe('Parse.File testing', () => {
notEqual(file.name(), "hello.txt");
done();
}
}));
}, done));
}
}));
}, done));
});
it("saving an already saved file", done => {
@@ -307,9 +340,9 @@ describe('Parse.File testing', () => {
equal(file.name(), previousName);
done();
}
}));
}, done));
}
}));
}, done));
});
it("two saves at the same time", done => {
@@ -337,11 +370,11 @@ describe('Parse.File testing', () => {
object.save({
file: file
}, expectSuccess({
success: function(obj) {
success: function() {
ok(object.toJSON().file.url);
done();
}
}));
}, done));
});
it("content-type used with no extension", done => {
@@ -358,7 +391,11 @@ describe('Parse.File testing', () => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(b.name).toMatch(/\.html$/);
request.get(b.url, (error, response, body) => {
request.get(b.url, (error, response) => {
if (!response) {
fail('response should be set');
return done();
}
expect(response.headers['content-type']).toMatch(/^text\/html/);
done();
});
@@ -463,10 +500,13 @@ describe('Parse.File testing', () => {
expect(fileAgain.name()).toEqual('meep');
expect(fileAgain.url()).toEqual('http://meep.meep');
done();
}).catch((e) => {
jfail(e);
done();
});
});
it('creates correct url for old files hosted on parse', done => {
it('creates correct url for old files hosted on files.parsetfss.com', done => {
var file = {
__type: 'File',
url: 'http://irrelevant.elephant/',
@@ -483,6 +523,32 @@ describe('Parse.File testing', () => {
'http://files.parsetfss.com/test/tfss-123.txt'
);
done();
}).catch((e) => {
jfail(e);
done();
});
});
it('creates correct url for old files hosted on files.parse.com', done => {
var file = {
__type: 'File',
url: 'http://irrelevant.elephant/',
name: 'd6e80979-a128-4c57-a167-302f874700dc-123.txt'
};
var obj = new Parse.Object('OldFileTest');
obj.set('oldfile', file);
obj.save().then(() => {
var query = new Parse.Query('OldFileTest');
return query.first();
}).then((result) => {
var fileAgain = result.get('oldfile');
expect(fileAgain.url()).toEqual(
'http://files.parse.com/test/d6e80979-a128-4c57-a167-302f874700dc-123.txt'
);
done();
}).catch((e) => {
jfail(e);
done();
});
});
@@ -497,9 +563,36 @@ describe('Parse.File testing', () => {
var query = new Parse.Query('FileTest');
return query.first();
}).then(result => {
let fileAgain = result.get('file');
const fileAgain = result.get('file');
expect(fileAgain.url()).toMatch(/123.txt$/);
done();
}).catch((e) => {
jfail(e);
done();
});
});
it('return with publicServerURL when provided', done => {
reconfigureServer({
publicServerURL: 'https://mydomain/parse'
}).then(() => {
var file = {
__type: 'File',
name: '123.txt'
};
var obj = new Parse.Object('FileTest');
obj.set('file', file);
return obj.save()
}).then(() => {
var query = new Parse.Query('FileTest');
return query.first();
}).then(result => {
const fileAgain = result.get('file');
expect(fileAgain.url().indexOf('https://mydomain/parse')).toBe(0);
done();
}).catch((e) => {
jfail(e);
done();
});
});
});
+18 -16
View File
@@ -76,7 +76,7 @@ describe('Parse.GeoPoint testing', () => {
obj.set('index', i);
objects.push(obj);
});
Parse.Object.saveAll(objects).then((list) => {
Parse.Object.saveAll(objects).then(() => {
var query = new Parse.Query(TestObject);
var point = new Parse.GeoPoint(1.0, -1.0);
query.withinRadians('location', point, 3.14);
@@ -85,8 +85,8 @@ describe('Parse.GeoPoint testing', () => {
equal(results.length, 3);
done();
}, (err) => {
console.log(err);
fail();
fail("Couldn't query GeoPoint");
jfail(err)
});
});
@@ -99,7 +99,7 @@ describe('Parse.GeoPoint testing', () => {
obj.set('index', i);
objects.push(obj);
});
Parse.Object.saveAll(objects, function(list) {
Parse.Object.saveAll(objects, function() {
var query = new Parse.Query(TestObject);
var point = new Parse.GeoPoint(1.0, -1.0);
query.withinRadians('location', point, 3.14 * 0.5);
@@ -123,7 +123,7 @@ describe('Parse.GeoPoint testing', () => {
obj.set('index', i);
objects.push(obj);
});
Parse.Object.saveAll(objects, function(list) {
Parse.Object.saveAll(objects, function() {
var query = new Parse.Query(TestObject);
var point = new Parse.GeoPoint(1.0, -1.0);
query.withinRadians('location', point, 3.14 * 0.25);
@@ -154,10 +154,11 @@ describe('Parse.GeoPoint testing', () => {
};
it('geo max distance in km everywhere', (done) => {
makeSomeGeoPoints(function(list) {
makeSomeGeoPoints(function() {
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
var query = new Parse.Query(TestObject);
query.withinKilometers('location', sfo, 4000.0);
// Honolulu is 4300 km away from SFO on a sphere ;)
query.withinKilometers('location', sfo, 4800.0);
query.find({
success: function(results) {
equal(results.length, 3);
@@ -168,7 +169,7 @@ describe('Parse.GeoPoint testing', () => {
});
it('geo max distance in km california', (done) => {
makeSomeGeoPoints(function(list) {
makeSomeGeoPoints(function() {
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
var query = new Parse.Query(TestObject);
query.withinKilometers('location', sfo, 3700.0);
@@ -184,7 +185,7 @@ describe('Parse.GeoPoint testing', () => {
});
it('geo max distance in km bay area', (done) => {
makeSomeGeoPoints(function(list) {
makeSomeGeoPoints(function() {
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
var query = new Parse.Query(TestObject);
query.withinKilometers('location', sfo, 100.0);
@@ -199,7 +200,7 @@ describe('Parse.GeoPoint testing', () => {
});
it('geo max distance in km mid peninsula', (done) => {
makeSomeGeoPoints(function(list) {
makeSomeGeoPoints(function() {
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
var query = new Parse.Query(TestObject);
query.withinKilometers('location', sfo, 10.0);
@@ -213,10 +214,10 @@ describe('Parse.GeoPoint testing', () => {
});
it('geo max distance in miles everywhere', (done) => {
makeSomeGeoPoints(function(list) {
makeSomeGeoPoints(function() {
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
var query = new Parse.Query(TestObject);
query.withinMiles('location', sfo, 2500.0);
query.withinMiles('location', sfo, 2600.0);
query.find({
success: function(results) {
equal(results.length, 3);
@@ -227,7 +228,7 @@ describe('Parse.GeoPoint testing', () => {
});
it('geo max distance in miles california', (done) => {
makeSomeGeoPoints(function(list) {
makeSomeGeoPoints(function() {
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
var query = new Parse.Query(TestObject);
query.withinMiles('location', sfo, 2200.0);
@@ -243,10 +244,11 @@ describe('Parse.GeoPoint testing', () => {
});
it('geo max distance in miles bay area', (done) => {
makeSomeGeoPoints(function(list) {
makeSomeGeoPoints(function() {
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
var query = new Parse.Query(TestObject);
query.withinMiles('location', sfo, 75.0);
// 100km is 62 miles...
query.withinMiles('location', sfo, 62.0);
query.find({
success: function(results) {
equal(results.length, 1);
@@ -258,7 +260,7 @@ describe('Parse.GeoPoint testing', () => {
});
it('geo max distance in miles mid peninsula', (done) => {
makeSomeGeoPoints(function(list) {
makeSomeGeoPoints(function() {
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
var query = new Parse.Query(TestObject);
query.withinMiles('location', sfo, 10.0);
+116 -25
View File
@@ -1,15 +1,26 @@
'use strict';
var request = require('request');
var Parse = require('parse/node').Parse;
let Config = require('../src/Config');
const Config = require('../src/Config');
describe('a GlobalConfig', () => {
beforeEach(done => {
let config = new Config('test');
config.database.adaptiveCollection('_GlobalConfig')
.then(coll => coll.upsertOne({ '_id': 1 }, { $set: { params: { companies: ['US', 'DK'] } } }))
.then(() => { done(); });
const config = new Config('test');
const query = on_db('mongo', () => {
// Legacy is with an int...
return { objectId: 1 };
}, () => {
return { objectId: "1" }
})
config.database.adapter.upsertOneObject(
'_GlobalConfig',
{ fields: { objectId: { type: 'Number' }, params: {type: 'Object'}} },
query,
{ params: { companies: ['US', 'DK'] } }
).then(done, (err) => {
jfail(err);
done();
});
});
it('can be retrieved', (done) => {
@@ -21,8 +32,10 @@ describe('a GlobalConfig', () => {
'X-Parse-Master-Key' : 'test'
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
expect(body.params.companies).toEqual(['US', 'DK']);
try {
expect(response.statusCode).toEqual(200);
expect(body.params.companies).toEqual(['US', 'DK']);
} catch(e) { jfail(e); }
done();
});
});
@@ -43,6 +56,80 @@ describe('a GlobalConfig', () => {
});
});
it('can add and retrive files', (done) => {
request.put({
url : 'http://localhost:8378/1/config',
json : true,
body : { params: { file: { __type: 'File', name: 'name', url: 'http://url' } } },
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key' : 'test'
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
expect(body.result).toEqual(true);
Parse.Config.get().then((res) => {
const file = res.get('file');
expect(file.name()).toBe('name');
expect(file.url()).toBe('http://url');
done();
});
});
});
it('can add and retrive Geopoints', (done) => {
const geopoint = new Parse.GeoPoint(10,-20);
request.put({
url : 'http://localhost:8378/1/config',
json : true,
body : { params: { point: geopoint.toJSON() } },
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key' : 'test'
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
expect(body.result).toEqual(true);
Parse.Config.get().then((res) => {
const point = res.get('point');
expect(point.latitude).toBe(10);
expect(point.longitude).toBe(-20);
done();
});
});
});
it('properly handles delete op', (done) => {
request.put({
url : 'http://localhost:8378/1/config',
json : true,
body : { params: { companies: {__op: 'Delete'}, foo: 'bar' } },
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key' : 'test'
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
expect(body.result).toEqual(true);
request.get({
url : 'http://localhost:8378/1/config',
json : true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key' : 'test'
}
}, (error, response, body) => {
try {
expect(response.statusCode).toEqual(200);
expect(body.params.companies).toBeUndefined();
expect(body.params.foo).toBe('bar');
expect(Object.keys(body.params).length).toBe(1);
} catch(e) { jfail(e); }
done();
});
});
});
it('fail to update if master key is missing', (done) => {
request.put({
url : 'http://localhost:8378/1/config',
@@ -60,23 +147,27 @@ describe('a GlobalConfig', () => {
});
it('failed getting config when it is missing', (done) => {
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,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key' : 'test'
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
expect(body.params).toEqual({});
done();
});
const config = new Config('test');
config.database.adapter.deleteObjectsByQuery(
'_GlobalConfig',
{ fields: { params: { __type: 'String' } } },
{ objectId: "1" }
).then(() => {
request.get({
url : 'http://localhost:8378/1/config',
json : true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key' : 'test'
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
expect(body.params).toEqual({});
done();
});
}).catch((e) => {
jfail(e);
done();
});
});
});
+468 -360
View File
@@ -1,390 +1,498 @@
"use strict";
/* 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 hookServerURL = "http://localhost:" + port;
const AppCache = require('../src/cache').AppCache;
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 have no hooks registered", (done) => {
Parse.Hooks.getFunctions().then((res) => {
expect(res.constructor).toBe(Array.prototype.constructor);
done();
}, (err) => {
jfail(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");
it("should have no triggers registered", (done) => {
Parse.Hooks.getTriggers().then((res) => {
expect(res.constructor).toBe(Array.prototype.constructor);
done();
}, (err) => {
jfail(err);
done();
});
});
it("should CRUD a function registration", (done) => {
// Create
Parse.Hooks.createFunction("My-Test-Function", "http://someurl")
.then(response => {
expect(response.functionName).toBe("My-Test-Function");
expect(response.url).toBe("http://someurl")
// Find
return Parse.Hooks.getFunction("My-Test-Function")
}).then(response => {
expect(response.objectId).toBeUndefined();
expect(response.url).toBe("http://someurl");
return Parse.Hooks.updateFunction("My-Test-Function", "http://anotherurl");
})
.then((res) => {
expect(res.objectId).toBeUndefined();
expect(res.functionName).toBe("My-Test-Function");
expect(res.url).toBe("http://anotherurl")
// delete
return Parse.Hooks.removeFunction("My-Test-Function")
})
.then(() => {
// Find again! but should be deleted
return Parse.Hooks.getFunction("My-Test-Function")
.then(res => {
fail("Failed to delete hook")
fail(res)
done();
return Promise.resolve();
}, (err) => {
expect(err).not.toBe(undefined);
expect(err.code).toBe(143);
expect(err.message).toBe("no function named: My-Test-Function is defined")
done();
return Promise.resolve();
})
})
.catch(error => {
jfail(error);
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.objectId).toBeUndefined();
expect(res.url).toBe("http://someurl");
// delete
return Parse.Hooks.updateTrigger("MyClass","beforeDelete", "http://anotherurl");
}, (err) => {
jfail(err);
done();
}).then((res) => {
expect(res.className).toBe("MyClass");
expect(res.url).toBe("http://anotherurl")
expect(res.objectId).toBeUndefined();
return Parse.Hooks.removeTrigger("MyClass","beforeDelete");
}, (err) => {
jfail(err);
done();
}).then(() => {
// Find again! but should be deleted
return Parse.Hooks.getTrigger("MyClass","beforeDelete");
}, (err) => {
jfail(err);
done();
}).then(function(){
fail("should not succeed");
done();
}, (err) => {
if (err) {
expect(err).not.toBe(null);
expect(err).not.toBe(undefined);
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.message).toBe("class MyClass does not exist")
} else {
fail('should have errored');
}
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(() => new Promise(resolve => setTimeout(resolve, 100)))
.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);
if (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.message).toBe('function name: my_new_function already exits')
}
return Parse.Hooks.removeFunction("my_new_function");
}).then(() => {
done();
}, (err) => {
jfail(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).not.toBe(undefined);
expect(err).not.toBe(null);
if (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.message).toBe('class MyClass already has trigger beforeSave')
}
return Parse.Hooks.removeTrigger("MyClass", "beforeSave");
}).then(() => {
done();
}, (err) => {
jfail(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).not.toBe(undefined);
expect(err).not.toBe(null);
if (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.message).toBe('no function named: A_COOL_FUNCTION is defined');
}
return Parse.Hooks.getFunction("A_COOL_FUNCTION")
}).then(() => {
fail("the function should not exist");
done();
}, (err) => {
expect(err).not.toBe(undefined);
expect(err).not.toBe(null);
if (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.message).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).not.toBe(undefined);
expect(err).not.toBe(null);
if (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.message).toBe('class AClassName does not exist');
}
return Parse.Hooks.getTrigger("AClassName","beforeSave")
}).then(() => {
fail("the function should not exist");
done();
}, (err) => {
expect(err).not.toBe(undefined);
expect(err).not.toBe(null);
if (err) {
expect(err.code).toBe(143);
expect(err.message).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).not.toBe(undefined);
expect(err).not.toBe(null);
if (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++) {
}
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(){
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!"});
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, AppCache.get('test').databaseController);
return hooksController.load()
}, (err) => {
jfail(err);
fail('Should properly create all hooks');
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) => {
jfail(err);
fail('should properly load all hooks');
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) => {
jfail(err);
fail("Should not fail creating a function");
done();
}).then(function(res){
expect(res).toBe("OK!");
done();
}, (err) => {
jfail(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 deleted 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) => {
jfail(err);
fail("Should not fail creating a function");
done();
}).then(function() {
fail("Should not succeed calling that function");
done();
}, (err) => {
expect(err).not.toBe(undefined);
expect(err).not.toBe(null);
if (err) {
expect(err.code).toBe(141);
expect(err.message.code).toEqual(1337)
expect(err.message.error).toEqual("hacking that one!");
}
done();
});
});
it("should provide X-Parse-Webhook-Key when defined", (done) => {
app.post("/ExpectingKey", function(req, res) {
if (req.get('X-Parse-Webhook-Key') === 'hook') {
res.json({success: "correct key provided"});
} else {
res.json({error: "incorrect key provided"});
}
});
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL + "/ExpectingKey").then(function(){
return Parse.Cloud.run("SOME_TEST_FUNCTION")
}, (err) => {
jfail(err);
fail("Should not fail creating a function");
done();
}).then(function(res){
expect(res).toBe("correct key provided");
done();
}, (err) => {
jfail(err);
fail("Should not fail calling a function");
done();
});
});
it("should not pass X-Parse-Webhook-Key if not provided", (done) => {
reconfigureServer({ webhookKey: undefined })
.then(() => {
app.post("/ExpectingKeyAlso", function(req, res) {
if (req.get('X-Parse-Webhook-Key') === 'hook') {
res.json({success: "correct key provided"});
} else {
res.json({error: "incorrect key provided"});
}
});
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL + "/ExpectingKeyAlso").then(function(){
return Parse.Cloud.run("SOME_TEST_FUNCTION")
}, (err) => {
jfail(err);
fail("Should not fail creating a function");
done();
}).then(function(){
fail("Should not succeed calling that function");
done();
}, (err) => {
expect(err).not.toBe(undefined);
expect(err).not.toBe(null);
if (err) {
expect(err.code).toBe(141);
expect(err.message).toEqual("incorrect key provided");
}
done();
});
});
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";
});
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(){
res.json({success: object});
});
// The function is deleted 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) => {
jfail(err);
fail("Should not fail creating a function");
done();
});
});
it("beforeSave hooks should correctly handle responses containing entire object", (done) => {
app.post("/BeforeSaveSome2", function(req, res) {
var object = Parse.Object.fromJSON(req.body.object);
object.set('hello', "world");
res.json({success: object});
});
Parse.Hooks.createTrigger("SomeRandomObject2", "beforeSave", hookServerURL + "/BeforeSaveSome2").then(function(){
const obj = new Parse.Object("SomeRandomObject2");
return obj.save();
}).then(function(res) {
return res.save();
}).then(function(res) {
expect(res.get("hello")).toEqual("world");
done();
}).fail((err) => {
fail(`Should not fail: ${JSON.stringify(err)}`);
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 deleted 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() {
var promise = new Parse.Promise();
// Wait a bit here as it's an after save
setTimeout(() => {
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();
});
});
});
new Parse.Query("AnotherObject")
.get(newObjectId)
.then((r) => promise.resolve(r));
}, 500);
return promise;
}).then(function(res){
expect(res.get("foo")).toEqual("bar");
done();
}).fail((err) => {
jfail(err);
fail("Should not fail creating a function");
done();
});
});
});
+391 -206
View File
@@ -2,18 +2,29 @@
// These tests check the Installations functionality of the REST API.
// Ported from installation_collection_test.go
var auth = require('../src/Auth');
var cache = require('../src/cache');
var Config = require('../src/Config');
var DatabaseAdapter = require('../src/DatabaseAdapter');
var Parse = require('parse/node').Parse;
var rest = require('../src/rest');
const auth = require('../src/Auth');
const Config = require('../src/Config');
const Parse = require('parse/node').Parse;
const rest = require('../src/rest');
const request = require("request");
var config = new Config('test');
let database = DatabaseAdapter.getDatabaseConnection('test', 'test_');
let config;
let database;
const defaultColumns = require('../src/Controllers/SchemaController').defaultColumns;
const delay = function delay(delay) {
return new Promise(resolve => setTimeout(resolve, delay));
}
const installationSchema = { fields: Object.assign({}, defaultColumns._Default, defaultColumns._Installation) };
describe('Installations', () => {
beforeEach(() => {
config = new Config('test');
database = config.database;
});
it('creates an android installation with ids', (done) => {
var installId = '12345678-abcd-abcd-abcd-123456789abc';
var device = 'android';
@@ -22,15 +33,14 @@ describe('Installations', () => {
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
var obj = results[0];
expect(obj.installationId).toEqual(installId);
expect(obj.deviceType).toEqual(device);
done();
}).catch((error) => { console.log(error); });
}).catch((error) => { console.log(error); jfail(error); done(); });
});
it('creates an ios installation with ids', (done) => {
@@ -41,15 +51,14 @@ describe('Installations', () => {
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
var obj = results[0];
expect(obj.deviceToken).toEqual(t);
expect(obj.deviceType).toEqual(device);
done();
}).catch((error) => { console.log(error); });
}).catch((error) => { console.log(error); jfail(error); done(); });
});
it('creates an embedded installation with ids', (done) => {
@@ -60,15 +69,14 @@ describe('Installations', () => {
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
var obj = results[0];
expect(obj.installationId).toEqual(installId);
expect(obj.deviceType).toEqual(device);
done();
}).catch((error) => { console.log(error); });
}).catch((error) => { console.log(error); jfail(error); done(); });
});
it('creates an android installation with all fields', (done) => {
@@ -80,9 +88,8 @@ describe('Installations', () => {
'channels': ['foo', 'bar']
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
var obj = results[0];
expect(obj.installationId).toEqual(installId);
@@ -92,7 +99,7 @@ describe('Installations', () => {
expect(obj.channels[0]).toEqual('foo');
expect(obj.channels[1]).toEqual('bar');
done();
}).catch((error) => { console.log(error); });
}).catch((error) => { console.log(error); jfail(error); done(); });
});
it('creates an ios installation with all fields', (done) => {
@@ -104,9 +111,8 @@ describe('Installations', () => {
'channels': ['foo', 'bar']
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
var obj = results[0];
expect(obj.deviceToken).toEqual(t);
@@ -116,7 +122,51 @@ describe('Installations', () => {
expect(obj.channels[0]).toEqual('foo');
expect(obj.channels[1]).toEqual('bar');
done();
}).catch((error) => { console.log(error); });
}).catch((error) => { console.log(error); jfail(error); done(); });
});
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(() => {
const query = new Parse.Query(Parse.Installation);
return query.find()
}).then(() => {
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(() => {
const 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(() => {
fail('Should not fail');
done();
});
});
it('fails with missing ids', (done) => {
@@ -158,10 +208,9 @@ describe('Installations', () => {
'channels': ['foo', 'bar'],
'custom': 'allowed'
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
var obj = results[0];
expect(obj.custom).toEqual('allowed');
@@ -174,7 +223,6 @@ describe('Installations', () => {
it('merging when installationId already exists', (done) => {
var installId1 = '12345678-abcd-abcd-abcd-123456789abc';
var t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
var installId2 = '12345678-abcd-abcd-abcd-123456789abd';
var input = {
'deviceToken': t,
'deviceType': 'ios',
@@ -184,18 +232,17 @@ describe('Installations', () => {
var firstObject;
var secondObject;
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
firstObject = results[0];
delete input.deviceToken;
delete input.channels;
input['foo'] = 'bar';
return rest.create(config, auth.nobody(config), '_Installation', input);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
secondObject = results[0];
expect(firstObject._id).toEqual(secondObject._id);
@@ -224,15 +271,14 @@ describe('Installations', () => {
var firstObject;
var secondObject;
rest.create(config, auth.nobody(config), '_Installation', input1)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
firstObject = results[0];
return rest.create(config, auth.nobody(config), '_Installation', input2);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(2);
if (results[0]['_id'] == firstObject._id) {
secondObject = results[1];
@@ -240,16 +286,19 @@ describe('Installations', () => {
secondObject = results[0];
}
return rest.create(config, auth.nobody(config), '_Installation', input3);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
expect(results[0]['_id']).toEqual(secondObject._id);
done();
}).catch((error) => { console.log(error); });
}).catch((error) => {
jfail(error);
done();
});
});
notWorking('creating multiple devices with same device token works', (done) => {
xit('creating multiple devices with same device token works', (done) => {
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
var installId3 = '33333333-abcd-abcd-abcd-123456789abc';
@@ -266,17 +315,14 @@ describe('Installations', () => {
}).then(() => {
input.installationId = installId3;
return rest.create(config, auth.nobody(config), '_Installation', input);
}).then(() => {
return database.mongoFind('_Installation',
{installationId: installId1}, {});
}).then((results) => {
})
.then(() => database.adapter.find('_Installation', {installationId: installId1}, installationSchema, {}))
.then(results => {
expect(results.length).toEqual(1);
return database.mongoFind('_Installation',
{installationId: installId2}, {});
}).then((results) => {
return database.adapter.find('_Installation', {installationId: installId2}, installationSchema, {});
}).then(results => {
expect(results.length).toEqual(1);
return database.mongoFind('_Installation',
{installationId: installId3}, {});
return database.adapter.find('_Installation', {installationId: installId3}, installationSchema, {});
}).then((results) => {
expect(results.length).toEqual(1);
done();
@@ -285,29 +331,30 @@ describe('Installations', () => {
it('updating with new channels', (done) => {
var input = {
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
'deviceType': 'android',
'channels': ['foo', 'bar']
installationId: '12345678-abcd-abcd-abcd-123456789abc',
deviceType: 'android',
channels: ['foo', 'bar']
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
var id = results[0]['_id'];
var id = results[0].objectId;
var update = {
'channels': ['baz']
};
return rest.update(config, auth.nobody(config),
'_Installation', id, update);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
return rest.update(config, auth.nobody(config), '_Installation', id, update);
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
expect(results[0].channels.length).toEqual(1);
expect(results[0].channels[0]).toEqual('baz');
done();
}).catch((error) => { console.log(error); });
}).catch(error => {
jfail(error);
done();
});
});
it('update android fails with new installation id', (done) => {
@@ -319,15 +366,11 @@ describe('Installations', () => {
'channels': ['foo', 'bar']
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
input = {
'installationId': installId2
};
return rest.update(config, auth.nobody(config), '_Installation',
results[0]['_id'], input);
input = { 'installationId': installId2 };
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
}).then(() => {
fail('Updating the installation should have failed.');
done();
@@ -346,15 +389,11 @@ describe('Installations', () => {
'channels': ['foo', 'bar']
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
input = {
'deviceToken': b
};
return rest.update(config, auth.nobody(config), '_Installation',
results[0]['_id'], input);
input = { 'deviceToken': b };
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
}).then(() => {
fail('Updating the installation should have failed.');
}).catch((error) => {
@@ -374,24 +413,25 @@ describe('Installations', () => {
'channels': ['foo', 'bar']
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
input = {
'installationId': installId,
'deviceToken': u,
'deviceType': 'ios'
};
return rest.update(config, auth.nobody(config), '_Installation',
results[0]['_id'], input);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
expect(results[0].deviceToken).toEqual(u);
done();
});
}).catch(err => {
jfail(err);
done();
})
});
it('update fails to change deviceType', (done) => {
@@ -402,15 +442,13 @@ describe('Installations', () => {
'channels': ['foo', 'bar']
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
input = {
'deviceType': 'ios'
};
return rest.update(config, auth.nobody(config), '_Installation',
results[0]['_id'], input);
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
}).then(() => {
fail('Should not have been able to update Installation.');
done();
@@ -428,18 +466,16 @@ describe('Installations', () => {
'channels': ['foo', 'bar']
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
input = {
'custom': 'allowed'
};
return rest.update(config, auth.nobody(config), '_Installation',
results[0]['_id'], input);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
expect(results[0]['custom']).toEqual('allowed');
done();
@@ -458,40 +494,39 @@ describe('Installations', () => {
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);
.then(() => {
input = {
'installationId': installId2,
'deviceType': 'android'
};
return rest.create(config, auth.nobody(config), '_Installation', input);
})
.then(() => database.adapter.find('_Installation', installationSchema, {installationId: installId1}, {}))
.then(results => {
firstObject = results[0];
return database.mongoFind('_Installation',
{installationId: installId2}, {});
}).then((results) => {
expect(results.length).toEqual(1);
return database.adapter.find('_Installation', installationSchema, {installationId: installId2}, {});
}).then(results => {
expect(results.length).toEqual(1);
secondObject = results[0];
// Update second installation to conflict with first installation
input = {
'objectId': secondObject._id,
'objectId': secondObject.objectId,
'deviceToken': t
};
return rest.update(config, auth.nobody(config), '_Installation',
secondObject._id, input);
}).then(() => {
return rest.update(config, auth.nobody(config), '_Installation', secondObject.objectId, input);
})
.then(() => database.adapter.find('_Installation', installationSchema, {objectId: firstObject.objectId}, {}))
.then(results => {
// 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); });
}).catch(error => {
jfail(error);
done();
});
});
it('update ios device token with duplicate device token', (done) => {
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
@@ -510,15 +545,16 @@ describe('Installations', () => {
'deviceType': 'ios'
};
return rest.create(config, auth.nobody(config), '_Installation', input);
}).then(() => {
return database.mongoFind('_Installation',
{installationId: installId1}, {});
}).then((results) => {
})
.then(() => delay(100))
.then(() => database.adapter.find('_Installation', installationSchema, {installationId: installId1}, {}))
.then((results) => {
expect(results.length).toEqual(1);
firstObject = results[0];
return database.mongoFind('_Installation',
{installationId: installId2}, {});
}).then((results) => {
})
.then(() => delay(100))
.then(() => database.adapter.find('_Installation', installationSchema, {installationId: installId2}, {}))
.then(results => {
expect(results.length).toEqual(1);
secondObject = results[0];
// Update second installation to conflict with first installation id
@@ -526,18 +562,21 @@ describe('Installations', () => {
'installationId': installId2,
'deviceToken': t
};
return rest.update(config, auth.nobody(config), '_Installation',
secondObject._id, input);
}).then(() => {
return rest.update(config, auth.nobody(config), '_Installation', secondObject.objectId, input);
})
.then(() => delay(100))
.then(() => database.adapter.find('_Installation', installationSchema, {objectId: firstObject.objectId}, {}))
.then(results => {
// 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); });
}).catch(error => {
jfail(error);
done();
});
});
notWorking('update ios device token with duplicate token different app', (done) => {
xit('update ios device token with duplicate token different app', (done) => {
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
@@ -552,13 +591,16 @@ describe('Installations', () => {
input.installationId = installId2;
input.appIdentifier = 'bar';
return rest.create(config, auth.nobody(config), '_Installation', input);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
// The first object should have been deleted during merge
expect(results.length).toEqual(1);
expect(results[0].installationId).toEqual(installId2);
done();
}).catch(error => {
jfail(error);
done();
});
});
@@ -570,24 +612,25 @@ describe('Installations', () => {
'deviceType': 'ios'
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
input = {
'deviceToken': t,
'channels': []
};
return rest.update(config, auth.nobody(config), '_Installation',
results[0]['_id'], input);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
expect(results[0].installationId).toEqual(installId);
expect(results[0].deviceToken).toEqual(t);
expect(results[0].channels.length).toEqual(0);
done();
}).catch(error => {
jfail(error);
done();
});
});
@@ -605,26 +648,27 @@ describe('Installations', () => {
'deviceType': 'ios'
};
return rest.create(config, auth.nobody(config), '_Installation', input);
}).then(() => {
return database.mongoFind('_Installation',
{deviceToken: t}, {});
}).then((results) => {
})
.then(() => database.adapter.find('_Installation', installationSchema, { deviceToken: t }, {}))
.then(results => {
expect(results.length).toEqual(1);
input = {
'deviceToken': t,
'installationId': installId,
'deviceType': 'ios'
};
return rest.update(config, auth.nobody(config), '_Installation',
results[0]['_id'], input);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
expect(results[0].installationId).toEqual(installId);
expect(results[0].deviceToken).toEqual(t);
expect(results[0].deviceType).toEqual('ios');
done();
}).catch(error => {
jfail(error);
done();
});
});
@@ -642,10 +686,9 @@ describe('Installations', () => {
'deviceType': 'ios'
};
return rest.create(config, auth.nobody(config), '_Installation', input);
}).then(() => {
return database.mongoFind('_Installation',
{deviceToken: t}, {});
}).then((results) => {
})
.then(() => database.adapter.find('_Installation', installationSchema, { deviceToken: t }, {}))
.then(results => {
expect(results.length).toEqual(1);
input = {
'deviceToken': t,
@@ -656,17 +699,19 @@ describe('Installations', () => {
'amount': 1
}
};
return rest.update(config, auth.nobody(config), '_Installation',
results[0]['_id'], input);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
expect(results[0].installationId).toEqual(installId);
expect(results[0].deviceToken).toEqual(t);
expect(results[0].deviceType).toEqual('ios');
expect(results[0].score).toEqual(1);
done();
}).catch(error => {
jfail(error);
done();
});
});
@@ -680,9 +725,8 @@ describe('Installations', () => {
var installObj;
var tokenObj;
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
installObj = results[0];
input = {
@@ -690,9 +734,9 @@ describe('Installations', () => {
'deviceType': 'ios'
};
return rest.create(config, auth.nobody(config), '_Installation', input);
}).then(() => {
return database.mongoFind('_Installation', {deviceToken: t}, {});
}).then((results) => {
})
.then(() => database.adapter.find('_Installation', installationSchema, { deviceToken: t }, {}))
.then(results => {
expect(results.length).toEqual(1);
tokenObj = results[0];
input = {
@@ -700,16 +744,18 @@ describe('Installations', () => {
'deviceToken': t,
'deviceType': 'ios'
};
return rest.update(config, auth.nobody(config), '_Installation',
installObj._id, input);
}).then(() => {
return database.mongoFind('_Installation', {_id: tokenObj._id}, {});
}).then((results) => {
return rest.update(config, auth.nobody(config), '_Installation', installObj.objectId, input);
})
.then(() => database.adapter.find('_Installation', installationSchema, { objectId: tokenObj.objectId }, {}))
.then(results => {
expect(results.length).toEqual(1);
expect(results[0].installationId).toEqual(installId);
expect(results[0].deviceToken).toEqual(t);
done();
}).catch((error) => { console.log(error); });
}).catch(error => {
jfail(error);
done();
});
});
it('update is linking two existing with installation id w/ op', (done) => {
@@ -722,9 +768,8 @@ describe('Installations', () => {
var installObj;
var tokenObj;
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
installObj = results[0];
input = {
@@ -732,9 +777,9 @@ describe('Installations', () => {
'deviceType': 'ios'
};
return rest.create(config, auth.nobody(config), '_Installation', input);
}).then(() => {
return database.mongoFind('_Installation', {deviceToken: t}, {});
}).then((results) => {
})
.then(() => database.adapter.find('_Installation', installationSchema, { deviceToken: t }, {}))
.then(results => {
expect(results.length).toEqual(1);
tokenObj = results[0];
input = {
@@ -746,17 +791,19 @@ describe('Installations', () => {
'amount': 1
}
};
return rest.update(config, auth.nobody(config), '_Installation',
installObj._id, input);
}).then(() => {
return database.mongoFind('_Installation', {_id: tokenObj._id}, {});
}).then((results) => {
return rest.update(config, auth.nobody(config), '_Installation', installObj.objectId, input);
})
.then(() => database.adapter.find('_Installation', installationSchema, { objectId: tokenObj.objectId }, {}))
.then(results => {
expect(results.length).toEqual(1);
expect(results[0].installationId).toEqual(installId);
expect(results[0].deviceToken).toEqual(t);
expect(results[0].score).toEqual(1);
done();
}).catch((error) => { console.log(error); });
}).catch(error => {
jfail(error);
done();
});
});
it('ios merge existing same token no installation id', (done) => {
@@ -777,9 +824,8 @@ describe('Installations', () => {
'deviceType': 'ios'
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
input = {
'installationId': installId,
@@ -787,18 +833,157 @@ describe('Installations', () => {
'deviceType': 'ios'
};
return rest.create(config, auth.nobody(config), '_Installation', input);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
expect(results[0].deviceToken).toEqual(t);
expect(results[0].installationId).toEqual(installId);
done();
})
.catch(error => {
console.log(error);
fail();
done();
});
});
it('allows you to get your own installation (regression test for #1718)', done => {
const installId = '12345678-abcd-abcd-abcd-123456789abc';
const device = 'android';
const input = {
'installationId': installId,
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(createResult => {
const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
request.get({
headers: headers,
url: 'http://localhost:8378/1/installations/' + createResult.response.objectId,
json: true,
}, (error, response, body) => {
expect(body.objectId).toEqual(createResult.response.objectId);
done();
});
})
.catch(error => {
console.log(error);
fail('failed');
done();
});
});
it('allows you to update installation from header (#2090)', done => {
const installId = '12345678-abcd-abcd-abcd-123456789abc';
const device = 'android';
const input = {
'installationId': installId,
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Installation-Id': installId
};
request.post({
headers: headers,
url: 'http://localhost:8378/1/classes/_Installation',
json: true,
body: {
date: new Date()
}
}, (error, response, body) => {
expect(response.statusCode).toBe(200);
expect(body.updatedAt).not.toBeUndefined();
done();
});
})
.catch(error => {
console.log(error);
fail('failed');
done();
});
});
it('allows you to update installation with masterKey', done => {
const installId = '12345678-abcd-abcd-abcd-123456789abc';
const device = 'android';
const input = {
'installationId': installId,
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(createResult => {
const installationObj = Parse.Installation.createWithoutData(createResult.response.objectId);
installationObj.set('customField', 'custom value');
return installationObj.save(null, {useMasterKey: true});
}).then(updateResult => {
expect(updateResult).not.toBeUndefined();
expect(updateResult.get('customField')).toEqual('custom value');
done();
}).catch(error => {
console.log(error);
fail('failed');
done();
});
});
it('should properly handle installation save #2780', done => {
const installId = '12345678-abcd-abcd-abcd-123456789abc';
const device = 'android';
const input = {
'installationId': installId,
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input).then(() => {
const query = new Parse.Query(Parse.Installation);
query.equalTo('installationId', installId);
query.first({useMasterKey: true}).then((installation) => {
return installation.save({
key: 'value'
}, {useMasterKey: true});
}).then(() => {
done();
}, (err) => {
jfail(err)
done();
});
});
});
it('should properly reject updating installationId', done => {
const installId = '12345678-abcd-abcd-abcd-123456789abc';
const device = 'android';
const input = {
'installationId': installId,
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input).then(() => {
const query = new Parse.Query(Parse.Installation);
query.equalTo('installationId', installId);
query.first({useMasterKey: true}).then((installation) => {
return installation.save({
key: 'value',
installationId: '22222222-abcd-abcd-abcd-123456789abc'
}, {useMasterKey: true});
}).then(() => {
fail('should not succeed');
done();
}, (err) => {
expect(err.code).toBe(136);
expect(err.message).toBe('installationId may not be changed in this operation');
done();
});
});
});
// TODO: Look at additional tests from installation_collection_test.go:882
// TODO: Do we need to support _tombstone disabling of installations?
// TODO: Test deletion, badge increments
});
+146 -9
View File
@@ -7,7 +7,6 @@ var testUserId = 'userId';
var testClassName = 'TestObject';
describe('ParseLiveQueryServer', function() {
beforeEach(function(done) {
// Mock ParseWebSocketServer
var mockParseWebSocketServer = jasmine.createSpy('ParseWebSocketServer');
@@ -160,7 +159,7 @@ describe('ParseLiveQueryServer', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Add two mock clients
var clientId = 1;
var client = addMockClient(parseLiveQueryServer, clientId);
addMockClient(parseLiveQueryServer, clientId);
var clientIdAgain = 2;
var clientAgain = addMockClient(parseLiveQueryServer, clientIdAgain);
// Add subscription for mock client 1
@@ -233,7 +232,7 @@ describe('ParseLiveQueryServer', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Add mock client
var clientId = 1;
var client = addMockClient(parseLiveQueryServer, clientId);
addMockClient(parseLiveQueryServer, clientId);
// Handle unsubscribe command
var parseWebSocket = {
clientId: 1
@@ -275,7 +274,7 @@ describe('ParseLiveQueryServer', function() {
expect(subscriptions.size).toBe(0);
});
it('can set connect command message handler for a parseWebSocket', function() {
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');
@@ -336,6 +335,35 @@ describe('ParseLiveQueryServer', function() {
expect(JSON.stringify(args[1])).toBe(unsubscribeRequest);
});
it('can set update command message handler for a parseWebSocket', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Register mock connect/subscribe/unsubscribe handler for the server
spyOn(parseLiveQueryServer, '_handleUpdateSubscription').and.callThrough();
spyOn(parseLiveQueryServer, '_handleUnsubscribe').and.callThrough();
spyOn(parseLiveQueryServer, '_handleSubscribe').and.callThrough();
// Make mock parseWebsocket
var EventEmitter = require('events');
var parseWebSocket = new EventEmitter();
// Register message handlers for the parseWebSocket
parseLiveQueryServer._onConnect(parseWebSocket);
// Check updateRequest request
var updateRequest = '{"op":"update"}';
// Trigger message event
parseWebSocket.emit('message', updateRequest);
// Make sure _handleUnsubscribe is called
var args = parseLiveQueryServer._handleUpdateSubscription.calls.mostRecent().args;
expect(args[0]).toBe(parseWebSocket);
expect(JSON.stringify(args[1])).toBe(updateRequest);
expect(parseLiveQueryServer._handleUnsubscribe).toHaveBeenCalled();
const unsubArgs = parseLiveQueryServer._handleUnsubscribe.calls.mostRecent().args;
expect(unsubArgs.length).toBe(3);
expect(unsubArgs[2]).toBe(false);
expect(parseLiveQueryServer._handleSubscribe).toHaveBeenCalled();
});
it('can set unknown command message handler for a parseWebSocket', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock parseWebsocket
@@ -505,7 +533,7 @@ describe('ParseLiveQueryServer', function() {
// 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){
parseLiveQueryServer._matchesSubscription = function(parseObject){
if (!parseObject) {
return false;
}
@@ -539,7 +567,7 @@ describe('ParseLiveQueryServer', function() {
var requestId = 2;
addMockSubscription(parseLiveQueryServer, clientId, requestId);
// Mock _matchesSubscription to return matching
parseLiveQueryServer._matchesSubscription = function(parseObject, subscription){
parseLiveQueryServer._matchesSubscription = function(parseObject){
if (!parseObject) {
return false;
}
@@ -575,7 +603,7 @@ describe('ParseLiveQueryServer', function() {
// 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){
parseLiveQueryServer._matchesSubscription = function(parseObject){
if (!parseObject) {
return false;
}
@@ -609,7 +637,7 @@ describe('ParseLiveQueryServer', function() {
var requestId = 2;
addMockSubscription(parseLiveQueryServer, clientId, requestId);
// Mock _matchesSubscription to return matching
parseLiveQueryServer._matchesSubscription = function(parseObject, subscription){
parseLiveQueryServer._matchesSubscription = function(parseObject){
if (!parseObject) {
return false;
}
@@ -718,7 +746,6 @@ describe('ParseLiveQueryServer', function() {
};
var requestId = 0;
var isChecked = false;
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
expect(isMatched).toBe(false);
done();
@@ -835,6 +862,116 @@ describe('ParseLiveQueryServer', function() {
});
});
it('won\'t match ACL that doesn\'t have public read or any roles', function(done){
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var acl = new Parse.ACL();
acl.setPublicReadAccess(false);
var client = {
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
sessionToken: 'sessionToken'
})
};
var requestId = 0;
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
expect(isMatched).toBe(false);
done();
});
});
it('won\'t match non-public ACL with role when there is no user', function(done){
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var acl = new Parse.ACL();
acl.setPublicReadAccess(false);
acl.setRoleReadAccess("livequery", true);
var client = {
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
})
};
var requestId = 0;
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
expect(isMatched).toBe(false);
done();
});
});
it('won\'t match ACL with role based read access set to false', function(done){
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var acl = new Parse.ACL();
acl.setPublicReadAccess(false);
acl.setRoleReadAccess("liveQueryRead", false);
var client = {
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
sessionToken: 'sessionToken'
})
};
var requestId = 0;
spyOn(Parse, "Query").and.callFake(function(){
return {
equalTo() {
// Nothing to do here
},
find() {
//Return a role with the name "liveQueryRead" as that is what was set on the ACL
var liveQueryRole = new Parse.Role();
liveQueryRole.set('name', 'liveQueryRead');
return [
liveQueryRole
];
}
}
});
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
expect(isMatched).toBe(false);
done();
});
});
it('will match ACL with role based read access set to true', function(done){
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var acl = new Parse.ACL();
acl.setPublicReadAccess(false);
acl.setRoleReadAccess("liveQueryRead", true);
var client = {
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
sessionToken: 'sessionToken'
})
};
var requestId = 0;
spyOn(Parse, "Query").and.callFake(function(){
return {
equalTo() {
// Nothing to do here
},
find() {
//Return a role with the name "liveQueryRead" as that is what was set on the ACL
var liveQueryRole = new Parse.Role();
liveQueryRole.set('name', 'liveQueryRead');
return [
liveQueryRole
];
}
}
});
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
expect(isMatched).toBe(true);
done();
});
});
it('can validate key when valid key is provided', function() {
var parseLiveQueryServer = new ParseLiveQueryServer({}, {
keyPairs: {
+151 -57
View File
@@ -14,7 +14,7 @@
describe('Parse.Object testing', () => {
it("create", function(done) {
create({ "test" : "test" }, function(model, response) {
create({ "test" : "test" }, function(model) {
ok(model.id, "Should have an objectId set");
equal(model.get("test"), "test", "Should have the right attribute");
done();
@@ -22,11 +22,11 @@ describe('Parse.Object testing', () => {
});
it("update", function(done) {
create({ "test" : "test" }, function(model, response) {
create({ "test" : "test" }, function(model) {
var t2 = new TestObject({ objectId: model.id });
t2.set("test", "changed");
t2.save(null, {
success: function(model, response) {
success: function(model) {
equal(model.get("test"), "changed", "Update should have succeeded");
done();
}
@@ -49,7 +49,7 @@ describe('Parse.Object testing', () => {
});
});
it("save cycle", function(done) {
it("save cycle", done => {
var a = new Parse.Object("TestObject");
var b = new Parse.Object("TestObject");
a.set("b", b);
@@ -72,10 +72,10 @@ describe('Parse.Object testing', () => {
});
it("get", function(done) {
create({ "test" : "test" }, function(model, response) {
create({ "test" : "test" }, function(model) {
var t2 = new TestObject({ objectId: model.id });
t2.fetch({
success: function(model2, response) {
success: function(model2) {
equal(model2.get("test"), "test", "Update should have succeeded");
ok(model2.id);
equal(model2.id, model.id, "Ids should match");
@@ -268,16 +268,21 @@ describe('Parse.Object testing', () => {
});
});
it("can set null", function(done) {
it_exclude_dbs(['postgres'])("can set null", function(done) {
var obj = new Parse.Object("TestObject");
obj.set("foo", null);
obj.save(null, {
success: function(obj) {
equal(obj.get("foo"), null);
on_db('mongo', () => {
equal(obj.get("foo"), null);
});
on_db('postgres', () => {
fail('should not succeed');
});
done();
},
error: function(obj, error) {
ok(false, error.message);
error: function() {
fail('should not fail');
done();
}
});
@@ -317,11 +322,11 @@ describe('Parse.Object testing', () => {
it("invalid class name", function(done) {
var item = new Parse.Object("Foo^bar");
item.save(null, {
success: function(item) {
success: function() {
ok(false, "The name should have been invalid.");
done();
},
error: function(item, error) {
error: function() {
// Because the class name is invalid, the router will not be able to route
// it, so it will actually return a -1 error code.
// equal(error.code, Parse.Error.INVALID_CLASS_NAME);
@@ -340,7 +345,6 @@ describe('Parse.Object testing', () => {
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', {
@@ -362,7 +366,7 @@ describe('Parse.Object testing', () => {
"foo": {
__type: "IvalidName"
}
}).then(fail, err => next(0));
}).then(fail, () => next(0));
});
it("simple field deletion", function(done) {
@@ -585,17 +589,27 @@ describe('Parse.Object testing', () => {
var objectId = x1.id;
var x2 = new Parse.Object('X', {objectId: objectId});
x2.addUnique('stuff', 2);
x2.addUnique('stuff', 3);
expect(x2.get('stuff')).toEqual([2, 3]);
x2.addUnique('stuff', 4);
expect(x2.get('stuff')).toEqual([2, 4]);
return x2.save();
}).then(() => {
var query = new Parse.Query('X');
return query.get(x1.id);
}).then((x3) => {
expect(x3.get('stuff')).toEqual([1, 2, 3]);
const stuff = x3.get('stuff');
const expected = [1, 2, 4];
expect(stuff.length).toBe(expected.length);
for (var i of stuff) {
expect(expected.indexOf(i) >= 0).toBe(true);
}
done();
}, (error) => {
fail(error);
on_db('mongo', () => {
jfail(error);
});
on_db('postgres', () => {
expect(error.message).toEqual("Postgres does not support AddUnique operator.");
});
done();
});
});
@@ -614,10 +628,21 @@ describe('Parse.Object testing', () => {
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'}]);
const stuff = x3.get('stuff');
const target = [1, {'hello': 'world'}, {'foo': 'bar'}, {'bar': 'baz'}];
expect(stuff.length).toEqual(target.length);
let found = 0;
for (const thing in target) {
for (const st in stuff) {
if (st == thing) {
found++;
}
}
}
expect(found).toBe(target.length);
done();
}, (error) => {
fail(error);
jfail(error);
done();
});
});
@@ -638,7 +663,7 @@ describe('Parse.Object testing', () => {
expect(x3.get('stuff')).toEqual([1, {'foo': 'bar'}]);
done();
}, (error) => {
fail(error);
jfail(error);
done();
});
});
@@ -661,7 +686,7 @@ describe('Parse.Object testing', () => {
done();
},
error: function(object, error) {
error: function() {
ok(false, "This should have saved.");
done();
}
@@ -1052,7 +1077,7 @@ describe('Parse.Object testing', () => {
parent.set('children', [child1, child2]);
parent.save(null, {
success: function(parent) {
success: function() {
var query = new Parse.Query(Child);
query.ascending('name');
query.find({
@@ -1172,8 +1197,7 @@ describe('Parse.Object testing', () => {
});
it("toJSON saved object", function(done) {
var _ = Parse._;
create({ "foo" : "bar" }, function(model, response) {
create({ "foo" : "bar" }, function(model) {
var objJSON = model.toJSON();
ok(objJSON.foo, "expected json to contain key 'foo'");
ok(objJSON.objectId, "expected json to contain key 'objectId'");
@@ -1247,7 +1271,7 @@ describe('Parse.Object testing', () => {
return bryan.save({
meal: "tomatoes"
});
}, function(error) {
}, function() {
ok(false, "Save should have succeeded.");
}).then(function() {
ok(false, "Save should have failed.");
@@ -1304,7 +1328,7 @@ describe('Parse.Object testing', () => {
});
});
it("bytes work", function(done) {
it_exclude_dbs(['postgres'])("bytes work", function(done) {
Parse.Promise.as().then(function() {
var obj = new TestObject();
obj.set("bytes", { __type: "Bytes", base64: "ZnJveW8=" });
@@ -1366,7 +1390,7 @@ describe('Parse.Object testing', () => {
}
equal(itemsAgain.length, numItems, "Should get the array back");
itemsAgain.forEach(function(item, i) {
var newValue = i*2;
var newValue = i * 2;
item.set("x", newValue);
});
return Parse.Object.saveAll(itemsAgain);
@@ -1376,7 +1400,7 @@ describe('Parse.Object testing', () => {
equal(fetchedItemsAgain.length, numItems,
"Number of items fetched should not change");
fetchedItemsAgain.forEach(function(item, i) {
equal(item.get("x"), i*2);
equal(item.get("x"), i * 2);
});
done();
});
@@ -1433,7 +1457,7 @@ describe('Parse.Object testing', () => {
}
equal(itemsAgain.length, numItems, "Should get the array back");
itemsAgain.forEach(function(item, i) {
var newValue = i*2;
var newValue = i * 2;
item.set("x", newValue);
});
return Parse.Object.saveAll(itemsAgain);
@@ -1443,11 +1467,11 @@ describe('Parse.Object testing', () => {
equal(fetchedItemsAgain.length, numItems,
"Number of items fetched should not change");
fetchedItemsAgain.forEach(function(item, i) {
equal(item.get("x"), i*2);
equal(item.get("x"), i * 2);
});
done();
},
error: function(error) {
error: function() {
ok(false, "Failed to fetchAll");
done();
}
@@ -1480,8 +1504,6 @@ describe('Parse.Object testing', () => {
it("fetchAll error on deleted object", function(done) {
var numItems = 11;
var container = new Container();
var subContainer = new Container();
var items = [];
for (var i = 0; i < numItems; i++) {
var item = new Item();
@@ -1504,7 +1526,7 @@ describe('Parse.Object testing', () => {
// TODO: Verify that with Sessions, this test is wrong... A fetch on
// user should not bring down a session token.
notWorking("fetchAll User attributes get merged", function(done) {
xit("fetchAll User attributes get merged", function(done) {
var sameUser;
var user = new Parse.User();
user.set("username", "asdf");
@@ -1559,7 +1581,7 @@ describe('Parse.Object testing', () => {
return;
}
itemsAgain.forEach(function(item, i) {
item.set("x", i*2);
item.set("x", i * 2);
});
return Parse.Object.saveAll(itemsAgain);
}).then(function() {
@@ -1597,7 +1619,7 @@ describe('Parse.Object testing', () => {
return;
}
itemsAgain.forEach(function(item, i) {
item.set("x", i*2);
item.set("x", i * 2);
});
return Parse.Object.saveAll(itemsAgain);
}).then(function() {
@@ -1612,7 +1634,7 @@ describe('Parse.Object testing', () => {
done();
},
error: function(error) {
error: function() {
ok(false, "Failed to fetchAll");
done();
}
@@ -1736,7 +1758,7 @@ describe('Parse.Object testing', () => {
var obj2 = new TestObject();
obj2.increment('astring');
return obj2.save();
}).then((obj2) => {
}).then(() => {
fail('Should not have saved.');
done();
}, (error) => {
@@ -1820,10 +1842,10 @@ describe('Parse.Object testing', () => {
"_nested": "key"
}
});
object.save().then( res => {
object.save().then(res => {
ok(res);
return res.fetch();
}).then( res => {
}).then(res => {
const foo = res.get("foo");
expect(foo["_bar"]).toEqual("_");
expect(foo["baz_bar"]).toEqual(1);
@@ -1831,34 +1853,40 @@ describe('Parse.Object testing', () => {
expect(foo["_0"]).toEqual("underscore_zero");
expect(foo["_more"]["_nested"]).toEqual("key");
done();
}).fail( err => {
console.error(err);
}).fail(err => {
jfail(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");
const obj1 = new Parse.Object("AnObject");
const obj2 = new Parse.Object("AnObject");
Parse.Object.saveAll([obj1, obj2]).then(() => {
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");
const 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");
if (res[0]) {
expect(res[0].get("obj")).toBe(undefined);
}
const query = new Parse.Query("AnObject");
return query.find();
}).then((res) => {
}).then((res) => {
expect(res.length).toBe(1);
expect(res[0].get("obj")).not.toBe(undefined);
return res[0].get("obj").fetch();
if (res[0]) {
expect(res[0].get("obj")).not.toBe(undefined);
return res[0].get("obj").fetch();
} else {
done();
}
}).then(() => {
fail("Should not fetch a deleted object");
}, (err) => {
@@ -1868,22 +1896,88 @@ describe('Parse.Object testing', () => {
});
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(() => {
const obj1 = new Parse.Object("AnObject");
const obj2 = new Parse.Object("AnObject");
const 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");
const 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();
}).catch(err => {
jfail(err);
done();
})
});
it('should handle includes on null arrays #2752', (done) => {
const obj1 = new Parse.Object("AnObject");
const obj2 = new Parse.Object("AnotherObject");
const obj3 = new Parse.Object("NestedObject");
obj3.set({
"foo": "bar"
})
obj2.set({
"key": obj3
})
Parse.Object.saveAll([obj1, obj2]).then(() => {
obj1.set("objects", [null, null, obj2]);
return obj1.save();
}).then(() => {
const query = new Parse.Query("AnObject");
query.include("objects.key");
return query.find();
}).then((res) => {
const obj = res[0];
expect(obj.get("objects")).not.toBe(undefined);
const array = obj.get("objects");
expect(Array.isArray(array)).toBe(true);
expect(array[0]).toBe(null);
expect(array[1]).toBe(null);
expect(array[2].get("key").get("foo")).toEqual("bar");
done();
}).catch(err => {
jfail(err);
done();
})
});
it('should handle select and include #2786', (done) => {
const score = new Parse.Object("GameScore");
const player = new Parse.Object("Player");
score.set({
"score": 1234
});
score.save().then(() => {
player.set("gameScore", score);
player.set("other", "value");
return player.save();
}).then(() => {
const query = new Parse.Query("Player");
query.include("gameScore");
query.select("gameScore");
return query.find();
}).then((res) => {
const obj = res[0];
const gameScore = obj.get("gameScore");
const other = obj.get("other");
expect(other).toBeUndefined();
expect(gameScore).not.toBeUndefined();
expect(gameScore.get("score")).toBe(1234);
done();
}).catch(err => {
jfail(err);
done();
})
});
});
+68 -18
View File
@@ -8,58 +8,108 @@ describe('ParsePubSub', function() {
createPublisher: jasmine.createSpy('createPublisherRedis'),
createSubscriber: jasmine.createSpy('createSubscriberRedis')
};
jasmine.mockLibrary('../src/LiveQuery/RedisPubSub', 'RedisPubSub', mockRedisPubSub);
jasmine.mockLibrary('../src/Adapters/PubSub/RedisPubSub', 'RedisPubSub', mockRedisPubSub);
// Mock EventEmitterPubSub
var mockEventEmitterPubSub = {
createPublisher: jasmine.createSpy('createPublisherEventEmitter'),
createSubscriber: jasmine.createSpy('createSubscriberEventEmitter')
};
jasmine.mockLibrary('../src/LiveQuery/EventEmitterPubSub', 'EventEmitterPubSub', mockEventEmitterPubSub);
jasmine.mockLibrary('../src/Adapters/PubSub/EventEmitterPubSub', 'EventEmitterPubSub', mockEventEmitterPubSub);
done();
});
it('can create redis publisher', function() {
var publisher = ParsePubSub.createPublisher({
ParsePubSub.createPublisher({
redisURL: 'redisURL'
});
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createPublisher).toHaveBeenCalledWith('redisURL');
var RedisPubSub = require('../src/Adapters/PubSub/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createPublisher).toHaveBeenCalledWith({redisURL: 'redisURL'});
expect(EventEmitterPubSub.createPublisher).not.toHaveBeenCalled();
});
it('can create event emitter publisher', function() {
var publisher = ParsePubSub.createPublisher({});
ParsePubSub.createPublisher({});
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
var RedisPubSub = require('../src/Adapters/PubSub/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createPublisher).not.toHaveBeenCalled();
expect(EventEmitterPubSub.createPublisher).toHaveBeenCalled();
});
it('can create redis subscriber', function() {
var subscriber = ParsePubSub.createSubscriber({
ParsePubSub.createSubscriber({
redisURL: 'redisURL'
});
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createSubscriber).toHaveBeenCalledWith('redisURL');
var RedisPubSub = require('../src/Adapters/PubSub/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createSubscriber).toHaveBeenCalledWith({redisURL: 'redisURL'});
expect(EventEmitterPubSub.createSubscriber).not.toHaveBeenCalled();
});
it('can create event emitter subscriber', function() {
var subscriptionInfos = ParsePubSub.createSubscriber({});
ParsePubSub.createSubscriber({});
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
var RedisPubSub = require('../src/Adapters/PubSub/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createSubscriber).not.toHaveBeenCalled();
expect(EventEmitterPubSub.createSubscriber).toHaveBeenCalled();
});
it('can create publisher/sub with custom adapter', function() {
const adapter = {
createPublisher: jasmine.createSpy('createPublisher'),
createSubscriber: jasmine.createSpy('createSubscriber')
}
ParsePubSub.createPublisher({
pubSubAdapter: adapter
});
expect(adapter.createPublisher).toHaveBeenCalled();
ParsePubSub.createSubscriber({
pubSubAdapter: adapter
});
expect(adapter.createSubscriber).toHaveBeenCalled();
var RedisPubSub = require('../src/Adapters/PubSub/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createSubscriber).not.toHaveBeenCalled();
expect(EventEmitterPubSub.createSubscriber).not.toHaveBeenCalled();
expect(RedisPubSub.createPublisher).not.toHaveBeenCalled();
expect(EventEmitterPubSub.createPublisher).not.toHaveBeenCalled();
});
it('can create publisher/sub with custom function adapter', function() {
const adapter = {
createPublisher: jasmine.createSpy('createPublisher'),
createSubscriber: jasmine.createSpy('createSubscriber')
}
ParsePubSub.createPublisher({
pubSubAdapter: function() {
return adapter;
}
});
expect(adapter.createPublisher).toHaveBeenCalled();
ParsePubSub.createSubscriber({
pubSubAdapter: function() {
return adapter;
}
});
expect(adapter.createSubscriber).toHaveBeenCalled();
var RedisPubSub = require('../src/Adapters/PubSub/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createSubscriber).not.toHaveBeenCalled();
expect(EventEmitterPubSub.createSubscriber).not.toHaveBeenCalled();
expect(RedisPubSub.createPublisher).not.toHaveBeenCalled();
expect(EventEmitterPubSub.createPublisher).not.toHaveBeenCalled();
});
afterEach(function(){
jasmine.restoreLibrary('../src/LiveQuery/RedisPubSub', 'RedisPubSub');
jasmine.restoreLibrary('../src/LiveQuery/EventEmitterPubSub', 'EventEmitterPubSub');
jasmine.restoreLibrary('../src/Adapters/PubSub/RedisPubSub', 'RedisPubSub');
jasmine.restoreLibrary('../src/Adapters/PubSub/EventEmitterPubSub', 'EventEmitterPubSub');
});
});
+766 -225
View File
File diff suppressed because it is too large Load Diff
+263 -141
View File
@@ -46,10 +46,10 @@ describe('Parse.Relation testing', () => {
var childObjects = [];
for (var i = 0; i < 10; i++) {
childObjects.push(new ChildObject({x:i}));
};
}
Parse.Object.saveAll(childObjects, expectSuccess({
success: function(list) {
success: function() {
var ParentObject = Parse.Object.extend("ParentObject");
var parent = new ParentObject();
parent.set("x", 4);
@@ -84,7 +84,7 @@ describe('Parse.Relation testing', () => {
}
Parse.Object.saveAll(childObjects, {
success: function(list) {
success: function() {
var ParentObject = Parse.Object.extend("ParentObject");
var parent = new ParentObject();
parent.set("x", 4);
@@ -106,7 +106,7 @@ describe('Parse.Relation testing', () => {
"The relation should not be dirty");
done();
},
error: function(list) {
error: function() {
ok(false, "This shouldn't have failed");
done();
}
@@ -131,7 +131,7 @@ describe('Parse.Relation testing', () => {
var parent;
var relation;
Parse.Object.saveAll(childObjects).then(function(list) {
Parse.Object.saveAll(childObjects).then(function() {
var ParentObject = Parse.Object.extend('ParentObject');
parent = new ParentObject();
parent.set('x', 4);
@@ -164,7 +164,7 @@ describe('Parse.Relation testing', () => {
});
it("queries with relations", (done) => {
it_exclude_dbs(['postgres'])("queries with relations", (done) => {
var ChildObject = Parse.Object.extend("ChildObject");
var childObjects = [];
@@ -248,7 +248,55 @@ describe('Parse.Relation testing', () => {
});
});
it("queries on relation fields with multiple ins", (done) => {
it("queries on relation fields with multiple containedIn (regression test for #1271)", (done) => {
const ChildObject = Parse.Object.extend("ChildObject");
const childObjects = [];
for (let i = 0; i < 10; i++) {
childObjects.push(new ChildObject({x: i}));
}
Parse.Object.saveAll(childObjects).then(() => {
const ParentObject = Parse.Object.extend("ParentObject");
const parent = new ParentObject();
parent.set("x", 4);
const parent1Children = parent.relation("child");
parent1Children.add(childObjects[0]);
parent1Children.add(childObjects[1]);
parent1Children.add(childObjects[2]);
const parent2 = new ParentObject();
parent2.set("x", 3);
const parent2Children = parent2.relation("child");
parent2Children.add(childObjects[4]);
parent2Children.add(childObjects[5]);
parent2Children.add(childObjects[6]);
const parent2OtherChildren = parent2.relation("otherChild");
parent2OtherChildren.add(childObjects[0]);
parent2OtherChildren.add(childObjects[1]);
parent2OtherChildren.add(childObjects[2]);
return Parse.Object.saveAll([parent, parent2]);
}).then(() => {
const 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(() => {
const 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_exclude_dbs(['postgres'])("query on pointer and relation fields with equal", (done) => {
var ChildObject = Parse.Object.extend("ChildObject");
var childObjects = [];
for (var i = 0; i < 10; i++) {
@@ -259,74 +307,33 @@ describe('Parse.Relation testing', () => {
var ParentObject = Parse.Object.extend("ParentObject");
var parent = new ParentObject();
parent.set("x", 4);
var relation = parent.relation("child");
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);
var relation2 = parent2.relation("child");
relation2.add(childObjects[4]);
relation2.add(childObjects[5]);
relation2.add(childObjects[6]);
var otherChild2 = parent2.relation("otherChild");
otherChild2.add(childObjects[0]);
otherChild2.add(childObjects[1]);
otherChild2.add(childObjects[2]);
parent2.set("toChild", childObjects[2]);
var parents = [];
parents.push(parent);
parents.push(parent2);
return Parse.Object.saveAll(parents);
}).then(() => {
var query = new Parse.Query(ParentObject);
var objects = [];
objects.push(childObjects[0]);
query.containedIn("child", objects);
query.containedIn("otherChild", [childObjects[0]]);
return query.find();
}).then((list) => {
equal(list.length, 2, "There should be 2 results");
done();
});
});
parents.push(new ParentObject());
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}));
}
return Parse.Object.saveAll(parents).then(() => {
var query = new Parse.Query(ParentObject);
query.equalTo("objectId", parent.id);
query.equalTo("toChilds", childObjects[2]);
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();
});
return query.find().then((list) => {
equal(list.length, 1, "There should be 1 result");
done();
});
});
}).catch(err => {
jfail(err);
done();
});
});
@@ -338,39 +345,39 @@ describe('Parse.Relation testing', () => {
}
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 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 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());
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);
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]);
query.equalTo("toChilds", childObjects[2]);
return query.find().then((list) => {
equal(list.length, 1, "There should be 1 result");
done();
});
return query.find().then((list) => {
equal(list.length, 1, "There should be 1 result");
done();
});
});
});
});
it("or queries on pointer and relation fields", (done) => {
it_exclude_dbs(['postgres'])("or queries on pointer and relation fields", (done) => {
var ChildObject = Parse.Object.extend("ChildObject");
var childObjects = [];
for (var i = 0; i < 10; i++) {
@@ -378,39 +385,39 @@ describe('Parse.Relation testing', () => {
}
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 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 parent2 = new ParentObject();
parent2.set("x", 3);
parent2.set("toChild", childObjects[2]);
var parents = [];
parents.push(parent);
parents.push(parent2);
parents.push(new ParentObject());
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();
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();
});
});
});
});
@@ -503,24 +510,24 @@ describe('Parse.Relation testing', () => {
});
it('should properly get related objects with unfetched queries', (done) => {
let objects = [];
let owners = [];
let allObjects = [];
const objects = [];
const owners = [];
const allObjects = [];
// Build 10 Objects and 10 owners
while (objects.length != 10) {
let object = new Parse.Object('AnObject');
const object = new Parse.Object('AnObject');
object.set({
index: objects.length,
even: objects.length % 2 == 0
});
objects.push(object);
let owner = new Parse.Object('AnOwner');
const owner = new Parse.Object('AnOwner');
owners.push(owner);
allObjects.push(object);
allObjects.push(owner);
}
let anotherOwner = new Parse.Object('AnotherOwner');
const anotherOwner = new Parse.Object('AnotherOwner');
return Parse.Object.saveAll(allObjects.concat([anotherOwner])).then(() => {
// put all the AnObject into the anotherOwner relationKey
@@ -532,42 +539,45 @@ describe('Parse.Relation testing', () => {
return Parse.Object.saveAll(owners.concat([anotherOwner]));
}).then(() => {
// Query on the relation of another owner
let object = new Parse.Object('AnotherOwner');
const object = new Parse.Object('AnotherOwner');
object.id = anotherOwner.id;
let relationQuery = object.relation('relationKey').query();
const 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');
const 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) => {
results.forEach((result) => {
expect(result.get('key').get('even')).toBe(true);
});
return Promise.resolve();
}).then(() => {
}).then(() => {
// Query on the relation of another owner
let object = new Parse.Object('AnotherOwner');
const object = new Parse.Object('AnotherOwner');
object.id = anotherOwner.id;
let relationQuery = object.relation('relationKey').query();
const 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');
const 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) => {
results.forEach((result) => {
expect(result.get('key').get('even')).toBe(false);
});
done();
}, (e) => {
fail(JSON.stringify(e));
done();
})
});
@@ -579,21 +589,19 @@ describe('Parse.Relation testing', () => {
new RestaurantObject({ ratings: 5, location: "Djibouti" }),
new RestaurantObject({ ratings: 3, location: "Ouagadougou" }),
];
let persons = [
const 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);
const owner = new OwnerObject({name: 'Joe'});
const 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();
const unfetchedOwner = new OwnerObject();
unfetchedOwner.id = owner.id;
var query = unfetchedOwner.relation('restaurants').query();
query.greaterThan("ratings", 4);
@@ -608,6 +616,9 @@ describe('Parse.Relation testing', () => {
done();
}
}));
}, (e) => {
fail(JSON.stringify(e));
done();
});
});
@@ -619,21 +630,19 @@ describe('Parse.Relation testing', () => {
new RestaurantObject({ ratings: 5, location: "Djibouti" }),
new RestaurantObject({ ratings: 3, location: "Ouagadougou" }),
];
let persons = [
const 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);
const owner = new OwnerObject({name: 'Joe'});
const 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();
const unfetchedOwner = new OwnerObject();
unfetchedOwner.id = owner.id;
var query = unfetchedOwner.relation('restaurants').query();
query.greaterThan("ratings", 4);
@@ -650,6 +659,119 @@ describe('Parse.Relation testing', () => {
done();
}
}));
}, (e) => {
fail(JSON.stringify(e));
done();
});
});
it('relations are not bidirectional (regression test for #871)', done => {
const PersonObject = Parse.Object.extend("Person");
const p1 = new PersonObject();
const p2 = new PersonObject();
Parse.Object.saveAll([p1, p2]).then(results => {
const p1 = results[0];
const p2 = results[1];
const relation = p1.relation('relation');
relation.add(p2);
p1.save().then(() => {
const query = new Parse.Query(PersonObject);
query.equalTo('relation', p1);
query.find().then(results => {
expect(results.length).toEqual(0);
const 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();
});
});
})
});
});
it('can query roles in Cloud Code (regession test #1489)', done => {
Parse.Cloud.define('isAdmin', (request, response) => {
const query = new Parse.Query(Parse.Role);
query.equalTo('name', 'admin');
query.first({ useMasterKey: true })
.then(role => {
const relation = new Parse.Relation(role, 'users');
const admins = relation.query();
admins.equalTo('username', request.user.get('username'));
admins.first({ useMasterKey: true })
.then(user => {
if (user) {
response.success(user);
done();
} else {
fail('Should have found admin user, found nothing instead');
done();
}
}, () => {
fail('User not admin');
done();
})
}, error => {
fail('Should have found admin user, errored instead');
fail(error);
done();
});
});
const adminUser = new Parse.User();
adminUser.set('username', 'name');
adminUser.set('password', 'pass');
adminUser.signUp()
.then(adminUser => {
const adminACL = new Parse.ACL();
adminACL.setPublicReadAccess(true);
// Create admin role
const adminRole = new Parse.Role('admin', adminACL);
adminRole.getUsers().add(adminUser);
adminRole.save()
.then(() => {
Parse.Cloud.run('isAdmin');
}, error => {
fail('failed to save role');
fail(error);
done()
});
}, error => {
fail('failed to sign up');
fail(error);
done();
});
});
it('can be saved without error', done => {
const obj1 = new Parse.Object('PPAP');
obj1.save()
.then(() => {
const newRelation = obj1.relation('aRelation');
newRelation.add(obj1);
obj1.save().then(() => {
const relation = obj1.get('aRelation');
obj1.set('aRelation', relation);
obj1.save().then(() => {
done();
}, error => {
fail('failed to save ParseRelation object');
fail(error);
done();
});
}, error => {
fail('failed to create relation field');
fail(error);
done();
});
}, error => {
fail('failed to save obj');
fail(error);
done();
});
});
});
+232 -81
View File
@@ -2,24 +2,27 @@
// 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', () => {
it('Do a bunch of basic role testing', (done) => {
it('Do a bunch of basic role testing', done => {
var user;
var role;
createTestUser().then((x) => {
user = x;
const acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setPublicWriteAccess(false);
role = new Parse.Object('_Role');
role.set('name', 'Foos');
role.setACL(acl);
var users = role.relation('users');
users.add(user);
return role.save({}, { useMasterKey: true });
}).then((x) => {
}).then(() => {
var query = new Parse.Query('_Role');
return query.find({ useMasterKey: true });
}).then((x) => {
@@ -39,7 +42,7 @@ describe('Parse Role testing', () => {
acl.setRoleWriteAccess('Foos', true);
obj.setACL(acl);
return obj.save();
}).then((x) => {
}).then(() => {
var query = new Parse.Query('TestObject');
return query.find({ sessionToken: user.getSessionToken() });
}).then((x) => {
@@ -52,69 +55,135 @@ describe('Parse Role testing', () => {
x.set('foo', 'baz');
// This should fail:
return x.save({},{sessionToken: ""});
}).then((x) => {
}).then(() => {
fail('Should not have been able to save.');
}, (e) => {
expect(e.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
done();
});
});
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, "_getAllRolesNamesForRoleIds").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
// 1 query for the parent roles
expect(restExecute.calls.count()).toEqual(2);
// 1 call for the 1st layer of roles
// 1 call for the 2nd layer
expect(getAllRolesSpy.calls.count()).toEqual(2);
done()
}).catch(() => {
fail("should succeed");
done();
});
});
it("should recursively load roles", (done) => {
var rolesNames = ["FooRole", "BarRole", "BazRole"];
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 });
}
var roleIds = {};
createTestUser().then( (user) => {
createTestUser().then((user) => {
// Put the user on the 1st role
return createRole(rolesNames[0], null, user).then( (aRole) => {
roleIds[aRole.get("name")] = aRole.id;
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;
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();
});
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(){
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) => {
.then(() => {
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)=>{
.then(()=>{
fail("_Role object should not save without ACL.");
}, (error2) =>{
expect(error2.code).toEqual(111);
@@ -122,59 +191,76 @@ describe('Parse Role testing', () => {
});
});
});
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(() => {
it("Different _Role objects cannot have the same name.", (done) => {
const roleName = "MyRole";
let aUser;
createTestUser().then((user) => {
aUser = user;
return createRole(roleName, null, aUser);
}).then((firstRole) => {
expect(firstRole.getName()).toEqual(roleName);
return createRole(roleName, null, aUser);
}).then(() => {
fail("_Role cannot have the same name as another role");
done();
}, (error) => {
expect(error.code).toEqual(137);
done();
});
});
it("Should properly resolve roles", (done) => {
const admin = new Parse.Role("Admin", new Parse.ACL());
const moderator = new Parse.Role("Moderator", new Parse.ACL());
const superModerator = new Parse.Role("SuperModerator", new Parse.ACL());
const contentManager = new Parse.Role('ContentManager', new Parse.ACL());
const 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(() => {
}).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) => {
const promises = [admin, moderator, contentManager, superModerator].map((role) => {
return auth._getAllRolesNamesForRoleIds([role.id]).then((result) => {
return Parse.Promise.as({
id: role.id,
name: role.get('name'),
roleIds: result
roleNames: result
});
})
});
return Parse.Promise.when(promises);
}).then((results) => {
results.forEach((result) => {
let id = result.id;
let roleIds = result.roleIds;
const id = result.id;
const roleNames = result.roleNames;
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);
expect(roleNames.length).toBe(2);
expect(roleNames.indexOf("Moderator")).not.toBe(-1);
expect(roleNames.indexOf("ContentManager")).not.toBe(-1);
} else if (id == moderator.id) {
expect(roleIds.length).toBe(1);
expect(roleIds.indexOf(contentManager.id)).toBe(0);
expect(roleNames.length).toBe(1);
expect(roleNames.indexOf("ContentManager")).toBe(0);
} else if (id == contentManager.id) {
expect(roleIds.length).toBe(0);
expect(roleNames.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);
expect(roleNames.length).toBe(3);
expect(roleNames.indexOf("Moderator")).not.toBe(-1);
expect(roleNames.indexOf("ContentManager")).not.toBe(-1);
expect(roleNames.indexOf("SuperContentManager")).not.toBe(-1);
}
});
done();
}).fail((err) => {
console.error(err);
}).fail(() => {
done();
})
});
it('can create role and query empty users', (done)=> {
@@ -182,17 +268,16 @@ describe('Parse Role testing', () => {
roleACL.setPublicReadAccess(true);
var role = new Parse.Role('subscribers', roleACL);
role.save({}, {useMasterKey : true})
.then((x)=>{
.then(()=>{
var query = role.relation('users').query();
query.find({useMasterKey : true})
.then((users)=>{
.then(()=>{
done();
}, (e)=>{
}, ()=>{
fail('should not have errors');
done();
});
}, (e) => {
console.log(e);
}, () => {
fail('should not have errored');
});
});
@@ -211,10 +296,10 @@ describe('Parse Role testing', () => {
user = x;
user2 = new Parse.User();
return user2.save({ username: 'user2', password: 'omgbbq' });
}).then((x) => {
}).then(() => {
user3 = new Parse.User();
return user3.save({ username: 'user3', password: 'omgbbq' });
}).then((x) => {
}).then(() => {
role = new Parse.Role('Admin', prACL);
role.getUsers().add(user);
return role.save({}, { useMasterKey: true });
@@ -260,7 +345,7 @@ describe('Parse Role testing', () => {
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(() => {
@@ -271,10 +356,76 @@ describe('Parse Role testing', () => {
fail('Customer user should not have been able to save.');
done();
}, (e) => {
expect(e.code).toEqual(101);
if (e) {
expect(e.code).toEqual(101);
} else {
fail('should return an error');
}
done();
})
});
});
it('should add multiple users to a role and remove users', (done) => {
var user, user2, user3;
var role;
var obj;
var prACL = new Parse.ACL();
prACL.setPublicReadAccess(true);
prACL.setPublicWriteAccess(true);
createTestUser().then((x) => {
user = x;
user2 = new Parse.User();
return user2.save({ username: 'user2', password: 'omgbbq' });
}).then(() => {
user3 = new Parse.User();
return user3.save({ username: 'user3', password: 'omgbbq' });
}).then(() => {
role = new Parse.Role('sharedRole', prACL);
var users = role.relation('users');
users.add(user);
users.add(user2);
users.add(user3);
return role.save({}, { useMasterKey: true });
}).then(() => {
// query for saved role and get 3 users
var query = new Parse.Query('_Role');
query.equalTo('name', 'sharedRole');
return query.find({ useMasterKey: true });
}).then((role) => {
expect(role.length).toEqual(1);
var users = role[0].relation('users').query();
return users.find({ useMasterKey: true });
}).then((users) => {
expect(users.length).toEqual(3);
obj = new Parse.Object('TestObjectRoles');
obj.set('ACL', prACL);
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('changedByUsers', true);
return obj.save(null, { sessionToken: user.getSessionToken() });
}).then(() => {
// query for saved role and get 3 users
var query = new Parse.Query('_Role');
query.equalTo('name', 'sharedRole');
return query.find({ useMasterKey: true });
}).then((role) => {
expect(role.length).toEqual(1);
var users = role[0].relation('users');
users.remove(user);
users.remove(user3);
return role[0].save({}, { useMasterKey: true });
}).then((role) =>{
var users = role.relation('users').query();
return users.find({ useMasterKey: true });
}).then((users) => {
expect(users.length).toEqual(1);
expect(users[0].get('username')).toEqual('user2');
done();
});
});
});
+134
View File
@@ -0,0 +1,134 @@
const ParseServerRESTController = require('../src/ParseServerRESTController').ParseServerRESTController;
const ParseServer = require('../src/ParseServer').default;
let RESTController;
describe('ParseServerRESTController', () => {
beforeEach(() => {
RESTController = ParseServerRESTController(Parse.applicationId, ParseServer.promiseRouter({appId: Parse.applicationId}));
})
it('should handle a get request', (done) => {
RESTController.request("GET", "/classes/MyObject").then((res) => {
expect(res.results.length).toBe(0);
done();
}, (err) => {
console.log(err);
jfail(err);
done();
});
});
it('should handle a get request with full serverURL mount path', (done) => {
RESTController.request("GET", "/1/classes/MyObject").then((res) => {
expect(res.results.length).toBe(0);
done();
}, (err) => {
jfail(err);
done();
});
});
it('should handle a POST batch', (done) => {
RESTController.request("POST", "batch", {
requests: [
{
method: 'GET',
path: '/classes/MyObject'
},
{
method: 'POST',
path: '/classes/MyObject',
body: {"key": "value"}
},
{
method: 'GET',
path: '/classes/MyObject'
}
]
}).then((res) => {
expect(res.length).toBe(3);
done();
}, (err) => {
jfail(err);
done();
});
});
it('should handle a POST request', (done) => {
RESTController.request("POST", "/classes/MyObject", {"key": "value"}).then(() => {
return RESTController.request("GET", "/classes/MyObject");
}).then((res) => {
expect(res.results.length).toBe(1);
expect(res.results[0].key).toEqual("value");
done();
}).fail((err) => {
console.log(err);
jfail(err);
done();
});
});
it('ensures sessionTokens are properly handled', (done) => {
let userId;
Parse.User.signUp('user', 'pass').then((user) => {
userId = user.id;
const sessionToken = user.getSessionToken();
return RESTController.request("GET", "/users/me", undefined, {sessionToken});
}).then((res) => {
// Result is in JSON format
expect(res.objectId).toEqual(userId);
done();
}).fail((err) => {
console.log(err);
jfail(err);
done();
});
});
it('ensures masterKey is properly handled', (done) => {
let userId;
Parse.User.signUp('user', 'pass').then((user) => {
userId = user.id;
return Parse.User.logOut().then(() => {
return RESTController.request("GET", "/classes/_User", undefined, {useMasterKey: true});
});
}).then((res) => {
expect(res.results.length).toBe(1);
expect(res.results[0].objectId).toEqual(userId);
done();
}, (err) => {
jfail(err);
done();
});
});
it('ensures no session token is created on creating users', (done) => {
RESTController.request("POST", "/classes/_User", {username: "hello", password: "world"}).then((user) => {
expect(user.sessionToken).toBeUndefined();
const query = new Parse.Query('_Session');
return query.find({useMasterKey: true});
}).then(sessions => {
expect(sessions.length).toBe(0);
done();
}, (err) => {
jfail(err);
done();
});
});
it('ensures a session token is created when passing installationId != cloud', (done) => {
RESTController.request("POST", "/classes/_User", {username: "hello", password: "world"}, {installationId: 'my-installation'}).then((user) => {
expect(user.sessionToken).not.toBeUndefined();
const query = new Parse.Query('_Session');
return query.find({useMasterKey: true});
}).then(sessions => {
expect(sessions.length).toBe(1);
expect(sessions[0].get('installationId')).toBe('my-installation');
done();
}, (err) => {
jfail(err);
done();
});
});
});
+829 -146
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -14,7 +14,9 @@ describe('ParseWebSocketServer', function() {
it('can handle connect event when ws is open', function(done) {
var onConnectCallback = jasmine.createSpy('onConnectCallback');
var parseWebSocketServer = new ParseWebSocketServer({}, onConnectCallback, 5).server;
var http = require('http');
var server = http.createServer();
var parseWebSocketServer = new ParseWebSocketServer(server, onConnectCallback, 5).server;
var ws = {
readyState: 0,
OPEN: 0,
@@ -27,6 +29,7 @@ describe('ParseWebSocketServer', function() {
// Make sure we ping to the client
setTimeout(function() {
expect(ws.ping).toHaveBeenCalled();
server.close();
done();
}, 10)
});
File diff suppressed because it is too large Load Diff
+692
View File
@@ -0,0 +1,692 @@
'use strict';
var Config = require('../src/Config');
describe('Pointer Permissions', () => {
beforeEach(() => {
new Config(Parse.applicationId).database.schemaCache.clear();
});
it('should work with find', (done) => {
const config = new Config(Parse.applicationId);
const user = new Parse.User();
const user2 = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
user2.set({
username: 'user2',
password: 'password'
});
const obj = new Parse.Object('AnObject');
const obj2 = new Parse.Object('AnObject');
Parse.Object.saveAll([user, user2]).then(() => {
obj.set('owner', user);
obj2.set('owner', user2);
return Parse.Object.saveAll([obj, obj2]);
}).then(() => {
return config.database.loadSchema().then((schema) => {
return schema.updateClass('AnObject', {}, {readUserFields: ['owner']})
});
}).then(() => {
return Parse.User.logIn('user1', 'password');
}).then(() => {
const q = new Parse.Query('AnObject');
return q.find();
}).then((res) => {
expect(res.length).toBe(1);
expect(res[0].id).toBe(obj.id);
done();
}).catch(error => {
fail(JSON.stringify(error));
done();
});
});
it('should work with write', (done) => {
const config = new Config(Parse.applicationId);
const user = new Parse.User();
const user2 = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
user2.set({
username: 'user2',
password: 'password'
});
const obj = new Parse.Object('AnObject');
const obj2 = new Parse.Object('AnObject');
Parse.Object.saveAll([user, user2]).then(() => {
obj.set('owner', user);
obj.set('reader', user2);
obj2.set('owner', user2);
obj2.set('reader', user);
return Parse.Object.saveAll([obj, obj2]);
}).then(() => {
return config.database.loadSchema().then((schema) => {
return schema.updateClass('AnObject', {}, {writeUserFields: ['owner'], readUserFields: ['reader', 'owner']});
});
}).then(() => {
return Parse.User.logIn('user1', 'password');
}).then(() => {
obj2.set('hello', 'world');
return obj2.save();
}).then(() => {
fail('User should not be able to update obj2');
}, (err) => {
// User 1 should not be able to update obj2
expect(err.code).toBe(101);
return Promise.resolve();
}).then(()=> {
obj.set('hello', 'world');
return obj.save();
}).then(() => {
return Parse.User.logIn('user2', 'password');
}, () => {
fail('User should be able to update');
return Promise.resolve();
}).then(() => {
const q = new Parse.Query('AnObject');
return q.find();
}, () => {
fail('should login with user 2');
}).then((res) => {
expect(res.length).toBe(2);
res.forEach((result) => {
if (result.id == obj.id) {
expect(result.get('hello')).toBe('world');
} else {
expect(result.id).toBe(obj2.id);
}
})
done();
}, () => {
fail("failed");
done();
})
});
it('should let a proper user find', (done) => {
const config = new Config(Parse.applicationId);
const user = new Parse.User();
const user2 = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
user2.set({
username: 'user2',
password: 'password'
});
const obj = new Parse.Object('AnObject');
const obj2 = new Parse.Object('AnObject');
user.signUp().then(() => {
return user2.signUp()
}).then(() => {
Parse.User.logOut();
}).then(() => {
obj.set('owner', user);
return Parse.Object.saveAll([obj, obj2]);
}).then(() => {
return config.database.loadSchema().then((schema) => {
return schema.updateClass('AnObject', {}, {find: {}, get:{}, readUserFields: ['owner']})
});
}).then(() => {
const q = new Parse.Query('AnObject');
return q.find();
}).then((res) => {
expect(res.length).toBe(0);
}).then(() => {
return Parse.User.logIn('user2', 'password');
}).then(() => {
const q = new Parse.Query('AnObject');
return q.find();
}).then((res) => {
expect(res.length).toBe(0);
const q = new Parse.Query('AnObject');
return q.get(obj.id);
}).then(() => {
fail('User 2 should not get the obj1 object');
}, (err) => {
expect(err.code).toBe(101);
expect(err.message).toBe('Object not found.');
return Promise.resolve();
}).then(() => {
return Parse.User.logIn('user1', 'password');
}).then(() => {
const q = new Parse.Query('AnObject');
return q.find();
}).then((res) => {
expect(res.length).toBe(1);
done();
}).catch((err) => {
jfail(err);
fail('should not fail');
done();
})
});
it('should not allow creating objects', (done) => {
const config = new Config(Parse.applicationId);
const user = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
const obj = new Parse.Object('AnObject');
user.save().then(() => {
return config.database.loadSchema().then((schema) => {
return schema.addClassIfNotExists('AnObject', {owner: {type:'Pointer', targetClass: '_User'}}, {create: {}, writeUserFields: ['owner'], readUserFields: ['owner']});
});
}).then(() => {
return Parse.User.logIn('user1', 'password');
}).then(() => {
obj.set('owner', user);
return obj.save();
}).then(() => {
fail('should not succeed');
done();
}, (err) => {
expect(err.code).toBe(119);
done();
})
});
it('should handle multiple writeUserFields', done => {
const config = new Config(Parse.applicationId);
const user = new Parse.User();
const user2 = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
user2.set({
username: 'user2',
password: 'password'
});
const obj = new Parse.Object('AnObject');
Parse.Object.saveAll([user, user2])
.then(() => {
obj.set('owner', user);
obj.set('otherOwner', user2);
return obj.save();
})
.then(() => config.database.loadSchema())
.then(schema => schema.updateClass('AnObject', {}, {find: {"*": true},writeUserFields: ['owner', 'otherOwner']}))
.then(() => Parse.User.logIn('user1', 'password'))
.then(() => obj.save({hello: 'fromUser1'}))
.then(() => Parse.User.logIn('user2', 'password'))
.then(() => obj.save({hello: 'fromUser2'}))
.then(() => Parse.User.logOut())
.then(() => {
const q = new Parse.Query('AnObject');
return q.first();
})
.then(result => {
expect(result.get('hello')).toBe('fromUser2');
done();
}).catch(() => {
fail('should not fail');
done();
})
});
it('should prevent creating pointer permission on missing field', (done) => {
const config = new Config(Parse.applicationId);
config.database.loadSchema().then((schema) => {
return schema.addClassIfNotExists('AnObject', {}, {create: {}, writeUserFields: ['owner'], readUserFields: ['owner']});
}).then(() => {
fail('should not succeed');
}).catch((err) => {
expect(err.code).toBe(107);
expect(err.message).toBe("'owner' is not a valid column for class level pointer permissions writeUserFields");
done();
})
});
it('should prevent creating pointer permission on bad field', (done) => {
const config = new Config(Parse.applicationId);
config.database.loadSchema().then((schema) => {
return schema.addClassIfNotExists('AnObject', {owner: {type: 'String'}}, {create: {}, writeUserFields: ['owner'], readUserFields: ['owner']});
}).then(() => {
fail('should not succeed');
}).catch((err) => {
expect(err.code).toBe(107);
expect(err.message).toBe("'owner' is not a valid column for class level pointer permissions writeUserFields");
done();
})
});
it('should prevent creating pointer permission on bad field', (done) => {
const config = new Config(Parse.applicationId);
const object = new Parse.Object('AnObject');
object.set('owner', 'value');
object.save().then(() => {
return config.database.loadSchema();
}).then((schema) => {
return schema.updateClass('AnObject', {}, {create: {}, writeUserFields: ['owner'], readUserFields: ['owner']});
}).then(() => {
fail('should not succeed');
}).catch((err) => {
expect(err.code).toBe(107);
expect(err.message).toBe("'owner' is not a valid column for class level pointer permissions writeUserFields");
done();
})
});
it('tests CLP / Pointer Perms / ACL write (PP Locked)', (done) => {
/*
tests:
CLP: update closed ({})
PointerPerm: "owner"
ACL: logged in user has access
The owner is another user than the ACL
*/
const config = new Config(Parse.applicationId);
const user = new Parse.User();
const user2 = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
user2.set({
username: 'user2',
password: 'password'
});
const obj = new Parse.Object('AnObject');
Parse.Object.saveAll([user, user2]).then(() => {
const ACL = new Parse.ACL();
ACL.setReadAccess(user, true);
ACL.setWriteAccess(user, true);
obj.setACL(ACL);
obj.set('owner', user2);
return obj.save();
}).then(() => {
return config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.updateClass('AnObject', {}, {update: {}, writeUserFields: ['owner']});
});
}).then(() => {
return Parse.User.logIn('user1', 'password');
}).then(() => {
// user1 has ACL read/write but should be blocked by PP
return obj.save({key: 'value'});
}).then(() => {
fail('Should not succeed saving');
done();
}, (err) => {
expect(err.code).toBe(101);
done();
});
});
it('tests CLP / Pointer Perms / ACL write (ACL Locked)', (done) => {
/*
tests:
CLP: update closed ({})
PointerPerm: "owner"
ACL: logged in user has access
*/
const config = new Config(Parse.applicationId);
const user = new Parse.User();
const user2 = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
user2.set({
username: 'user2',
password: 'password'
});
const obj = new Parse.Object('AnObject');
Parse.Object.saveAll([user, user2]).then(() => {
const ACL = new Parse.ACL();
ACL.setReadAccess(user, true);
ACL.setWriteAccess(user, true);
obj.setACL(ACL);
obj.set('owner', user2);
return obj.save();
}).then(() => {
return config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.updateClass('AnObject', {}, {update: {}, writeUserFields: ['owner']});
});
}).then(() => {
return Parse.User.logIn('user2', 'password');
}).then(() => {
// user1 has ACL read/write but should be blocked by ACL
return obj.save({key: 'value'});
}).then(() => {
fail('Should not succeed saving');
done();
}, (err) => {
expect(err.code).toBe(101);
done();
});
});
it('tests CLP / Pointer Perms / ACL write (ACL/PP OK)', (done) => {
/*
tests:
CLP: update closed ({})
PointerPerm: "owner"
ACL: logged in user has access
*/
const config = new Config(Parse.applicationId);
const user = new Parse.User();
const user2 = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
user2.set({
username: 'user2',
password: 'password'
});
const obj = new Parse.Object('AnObject');
Parse.Object.saveAll([user, user2]).then(() => {
const ACL = new Parse.ACL();
ACL.setWriteAccess(user, true);
ACL.setWriteAccess(user2, true);
obj.setACL(ACL);
obj.set('owner', user2);
return obj.save();
}).then(() => {
return config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.updateClass('AnObject', {}, {update: {}, writeUserFields: ['owner']});
});
}).then(() => {
return Parse.User.logIn('user2', 'password');
}).then(() => {
// user1 has ACL read/write but should be blocked by ACL
return obj.save({key: 'value'});
}).then((objAgain) => {
expect(objAgain.get('key')).toBe('value');
done();
}, () => {
fail('Should not fail saving');
done();
});
});
it('tests CLP / Pointer Perms / ACL read (PP locked)', (done) => {
/*
tests:
CLP: find/get open ({})
PointerPerm: "owner" : read
ACL: logged in user has access
The owner is another user than the ACL
*/
const config = new Config(Parse.applicationId);
const user = new Parse.User();
const user2 = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
user2.set({
username: 'user2',
password: 'password'
});
const obj = new Parse.Object('AnObject');
Parse.Object.saveAll([user, user2]).then(() => {
const ACL = new Parse.ACL();
ACL.setReadAccess(user, true);
ACL.setWriteAccess(user, true);
obj.setACL(ACL);
obj.set('owner', user2);
return obj.save();
}).then(() => {
return config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.updateClass('AnObject', {}, {find: {}, get: {}, readUserFields: ['owner']});
});
}).then(() => {
return Parse.User.logIn('user1', 'password');
}).then(() => {
// user1 has ACL read/write but should be block
return obj.fetch();
}).then(() => {
fail('Should not succeed saving');
done();
}, (err) => {
expect(err.code).toBe(101);
done();
});
});
it('tests CLP / Pointer Perms / ACL read (PP/ACL OK)', (done) => {
/*
tests:
CLP: find/get open ({"*": true})
PointerPerm: "owner" : read
ACL: logged in user has access
*/
const config = new Config(Parse.applicationId);
const user = new Parse.User();
const user2 = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
user2.set({
username: 'user2',
password: 'password'
});
const obj = new Parse.Object('AnObject');
Parse.Object.saveAll([user, user2]).then(() => {
const ACL = new Parse.ACL();
ACL.setReadAccess(user, true);
ACL.setWriteAccess(user, true);
ACL.setReadAccess(user2, true);
ACL.setWriteAccess(user2, true);
obj.setACL(ACL);
obj.set('owner', user2);
return obj.save();
}).then(() => {
return config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.updateClass('AnObject', {}, {find: {"*": true}, get: {"*": true}, readUserFields: ['owner']});
});
}).then(() => {
return Parse.User.logIn('user2', 'password');
}).then(() => {
// user1 has ACL read/write but should be block
return obj.fetch();
}).then((objAgain) => {
expect(objAgain.id).toBe(obj.id);
done();
}, () => {
fail('Should not fail fetching');
done();
});
});
it('tests CLP / Pointer Perms / ACL read (ACL locked)', (done) => {
/*
tests:
CLP: find/get open ({"*": true})
PointerPerm: "owner" : read // proper owner
ACL: logged in user has not access
*/
const config = new Config(Parse.applicationId);
const user = new Parse.User();
const user2 = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
user2.set({
username: 'user2',
password: 'password'
});
const obj = new Parse.Object('AnObject');
Parse.Object.saveAll([user, user2]).then(() => {
const ACL = new Parse.ACL();
ACL.setReadAccess(user, true);
ACL.setWriteAccess(user, true);
obj.setACL(ACL);
obj.set('owner', user2);
return obj.save();
}).then(() => {
return config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.updateClass('AnObject', {}, {find: {"*": true}, get: {"*": true}, readUserFields: ['owner']});
});
}).then(() => {
return Parse.User.logIn('user2', 'password');
}).then(() => {
// user2 has ACL read/write but should be block by ACL
return obj.fetch();
}).then(() => {
fail('Should not succeed saving');
done();
}, (err) => {
expect(err.code).toBe(101);
done();
});
});
it('should let master key find objects', (done) => {
const config = new Config(Parse.applicationId);
const object = new Parse.Object('AnObject');
object.set('hello', 'world');
return object.save().then(() => {
return config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.updateClass('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {find: {}, get: {}, readUserFields: ['owner']});
});
}).then(() => {
const q = new Parse.Query('AnObject');
return q.find();
}).then(() => {
}, (err) => {
expect(err.code).toBe(101);
return Promise.resolve();
}).then(() => {
const q = new Parse.Query('AnObject');
return q.find({useMasterKey: true});
}).then((objects) => {
expect(objects.length).toBe(1);
done();
}, () => {
fail('master key should find the object');
done();
})
});
it('should let master key get objects', (done) => {
const config = new Config(Parse.applicationId);
const object = new Parse.Object('AnObject');
object.set('hello', 'world');
return object.save().then(() => {
return config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.updateClass('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {find: {}, get: {}, readUserFields: ['owner']});
});
}).then(() => {
const q = new Parse.Query('AnObject');
return q.get(object.id);
}).then(() => {
}, (err) => {
expect(err.code).toBe(101);
return Promise.resolve();
}).then(() => {
const q = new Parse.Query('AnObject');
return q.get(object.id, {useMasterKey: true});
}).then((objectAgain) => {
expect(objectAgain).not.toBeUndefined();
expect(objectAgain.id).toBe(object.id);
done();
}, () => {
fail('master key should find the object');
done();
})
});
it('should let master key update objects', (done) => {
const config = new Config(Parse.applicationId);
const object = new Parse.Object('AnObject');
object.set('hello', 'world');
return object.save().then(() => {
return config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.updateClass('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {update: {}, writeUserFields: ['owner']});
});
}).then(() => {
return object.save({'hello': 'bar'});
}).then(() => {
}, (err) => {
expect(err.code).toBe(101);
return Promise.resolve();
}).then(() => {
return object.save({'hello': 'baz'}, {useMasterKey: true});
}).then((objectAgain) => {
expect(objectAgain.get('hello')).toBe('baz');
done();
}, () => {
fail('master key should save the object');
done();
})
});
it('should let master key delete objects', (done) => {
const config = new Config(Parse.applicationId);
const object = new Parse.Object('AnObject');
object.set('hello', 'world');
return object.save().then(() => {
return config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.updateClass('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {delete: {}, writeUserFields: ['owner']});
});
}).then(() => {
return object.destroy();
}).then(() => {
fail();
}, (err) => {
expect(err.code).toBe(101);
return Promise.resolve();
}).then(() => {
return object.destroy({useMasterKey: true});
}).then(() => {
done();
}, () => {
fail('master key should destroy the object');
done();
})
});
it('should fail with invalid pointer perms', (done) => {
const config = new Config(Parse.applicationId);
config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.addClassIfNotExists('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {delete: {}, writeUserFields: 'owner'});
}).catch((err) => {
expect(err.code).toBe(Parse.Error.INVALID_JSON);
done();
});
});
it('should fail with invalid pointer perms', (done) => {
const config = new Config(Parse.applicationId);
config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.addClassIfNotExists('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {delete: {}, writeUserFields: ['owner', 'invalid']});
}).catch((err) => {
expect(err.code).toBe(Parse.Error.INVALID_JSON);
done();
});
})
});
+65
View File
@@ -0,0 +1,65 @@
const parser = require('../src/Adapters/Storage/Postgres/PostgresConfigParser');
const queryParamTests = {
'a=1&b=2': { a: '1', b: '2' },
'a=abcd%20efgh&b=abcd%3Defgh': { a: 'abcd efgh', b: 'abcd=efgh' },
'a=1&b&c=true': { a: '1', b: '', c: 'true' }
}
describe('PostgresConfigParser.parseQueryParams', () => {
it('creates a map from a query string', () => {
for (const key in queryParamTests) {
const result = parser.parseQueryParams(key);
const testObj = queryParamTests[key];
expect(Object.keys(result).length)
.toEqual(Object.keys(testObj).length);
for (const k in result) {
expect(result[k]).toEqual(testObj[k]);
}
}
})
});
const baseURI = 'postgres://username:password@localhost:5432/db-name'
const dbOptionsTest = {};
dbOptionsTest[`${baseURI}?ssl=true&binary=true&application_name=app_name&fallback_application_name=f_app_name&poolSize=10`] = {
ssl: true,
binary: true,
application_name: 'app_name',
fallback_application_name: 'f_app_name',
poolSize: 10
};
dbOptionsTest[`${baseURI}?ssl=&binary=aa`] = {
ssl: false,
binary: false
}
describe('PostgresConfigParser.getDatabaseOptionsFromURI', () => {
it('creates a db options map from a query string', () => {
for (const key in dbOptionsTest) {
const result = parser.getDatabaseOptionsFromURI(key);
const testObj = dbOptionsTest[key];
for (const k in testObj) {
expect(result[k]).toEqual(testObj[k]);
}
}
});
it('sets the poolSize to 10 if the it is not a number', () => {
const result = parser.getDatabaseOptionsFromURI(`${baseURI}?poolSize=sdf`);
expect(result.poolSize).toEqual(10);
});
});
+7 -8
View File
@@ -1,20 +1,19 @@
var PromiseRouter = require("../src/PromiseRouter").default;
describe("PromiseRouter", () => {
it("should properly handle rejects", (done) => {
describe("PromiseRouter", () => {
it("should properly handle rejects", (done) => {
var router = new PromiseRouter();
router.route("GET", "/dummy", (req)=> {
router.route("GET", "/dummy", ()=> {
return Promise.reject({
error: "an error",
code: -1
})
}, (req) => {
}, () => {
fail("this should not be called");
});
router.routes[0].handler({}).then((result) => {
console.error(result);
jfail(result);
fail("this should not be called");
done();
}, (error)=> {
@@ -23,4 +22,4 @@ describe("PromiseRouter", () => {
done();
});
});
})
})
+25 -46
View File
@@ -2,45 +2,35 @@
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) => {
request('http://localhost:8378/1/apps/invalid_link.html', (err, httpResponse) => {
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();
});
reconfigureServer({
appName: 'unused',
publicServerURL: 'http://localhost:8378/1',
})
.then(() => {
request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse) => {
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) => {
request('http://localhost:8378/1/apps/verify_email_success.html', (err, httpResponse) => {
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) => {
request('http://localhost:8378/1/apps/password_reset_success.html', (err, httpResponse) => {
expect(httpResponse.statusCode).toBe(200);
done();
});
@@ -48,37 +38,26 @@ describe("public API", () => {
});
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();
})
beforeEach(done => {
reconfigureServer({ appName: 'unused' })
.then(done, fail);
});
it("should get 404 on verify_email", (done) => {
request('http://localhost:8378/1/apps/test/verify_email', (err, httpResponse, body) => {
request('http://localhost:8378/1/apps/test/verify_email', (err, httpResponse) => {
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) => {
request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse) => {
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) => {
request('http://localhost:8378/1/apps/test/request_password_reset', (err, httpResponse) => {
expect(httpResponse.statusCode).toBe(404);
done();
});
+66 -67
View File
@@ -1,7 +1,5 @@
var request = require("request");
function createProduct() {
const file = new Parse.File("name", {
base64: new Buffer("download_file", "utf-8").toString("base64")
@@ -9,7 +7,7 @@ function createProduct() {
return file.save().then(function(){
var product = new Parse.Object("_Product");
product.set({
download: file,
download: file,
icon: file,
title: "a product",
subtitle: "a product",
@@ -18,29 +16,26 @@ function createProduct() {
})
return product.save();
})
}
describe("test validate_receipt endpoint", () => {
beforeEach( done => {
createProduct().then(done).fail(function(err){
console.error(err);
beforeEach(done => {
createProduct().then(done).fail(function(){
done();
})
});
})
it("should bypass appstore validation", (done) => {
request.post({
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",
productIdentifier: "a-product",
receipt: {
__type: "Bytes",
base64: new Buffer("receipt", "utf-8").toString("base64")
@@ -50,7 +45,7 @@ describe("test validate_receipt endpoint", () => {
}, function(err, res, body){
if (typeof body != "object") {
fail("Body is not an object");
done();
done();
} else {
expect(body.__type).toEqual("File");
const url = body.url;
@@ -63,31 +58,31 @@ describe("test validate_receipt endpoint", () => {
}
});
});
it("should fail for missing receipt", (done) => {
request.post({
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",
productIdentifier: "a-product",
bypassAppStoreValidation: true
}
}, function(err, res, body){
if (typeof body != "object") {
fail("Body is not an object");
done();
done();
} else {
expect(body.code).toEqual(Parse.Error.INVALID_JSON);
done();
}
});
});
it("should fail for missing product identifier", (done) => {
request.post({
request.post({
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'},
@@ -103,24 +98,24 @@ describe("test validate_receipt endpoint", () => {
}, function(err, res, body){
if (typeof body != "object") {
fail("Body is not an object");
done();
done();
} else {
expect(body.code).toEqual(Parse.Error.INVALID_JSON);
done();
}
});
});
it("should bypass appstore validation and not find product", (done) => {
request.post({
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",
productIdentifier: "another-product",
receipt: {
__type: "Bytes",
base64: new Buffer("receipt", "utf-8").toString("base64")
@@ -138,17 +133,16 @@ describe("test validate_receipt endpoint", () => {
}
});
});
it("should fail at appstore validation", (done) => {
request.post({
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",
productIdentifier: "a-product",
receipt: {
__type: "Bytes",
base64: new Buffer("receipt", "utf-8").toString("base64")
@@ -164,46 +158,51 @@ describe("test validate_receipt endpoint", () => {
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();
})
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();
});
var query = new Parse.Query("_Product");
query.first().then(function(product) {
if (!product) {
return Promise.reject(new Error('Product should be found'));
}
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();
});
var query = new Parse.Query("_Product");
query.first().then(function(product){
if (!product) {
return Promise.reject(new Error('Product should be found'));
}
product.unset("title");
return product.save();
}).then(function(){
fail("Should not succeed");
done();
}).fail(function(err){
expect(err.code).toEqual(Parse.Error.INCORRECT_TYPE);
expect(err.message).toEqual("title is required.");
done();
});
});
});
+357 -186
View File
@@ -1,11 +1,12 @@
"use strict";
var PushController = require('../src/Controllers/PushController').PushController;
var pushStatusHandler = require('../src/pushStatusHandler');
var StatusHandler = require('../src/StatusHandler');
var Config = require('../src/Config');
var validatePushType = require('../src/Push/utils').validatePushType;
const successfulTransmissions = function(body, installations) {
let promises = installations.map((device) => {
const promises = installations.map((device) => {
return Promise.resolve({
transmitted: true,
device: device,
@@ -17,7 +18,7 @@ const successfulTransmissions = function(body, installations) {
const successfulIOS = function(body, installations) {
let promises = installations.map((device) => {
const promises = installations.map((device) => {
return Promise.resolve({
transmitted: device.deviceType == "ios",
device: device,
@@ -35,7 +36,7 @@ describe('PushController', () => {
var validPushTypes = ['ios', 'android'];
expect(function(){
PushController.validatePushType(where, validPushTypes);
validatePushType(where, validPushTypes);
}).not.toThrow();
done();
});
@@ -48,7 +49,7 @@ describe('PushController', () => {
var validPushTypes = ['ios', 'android'];
expect(function(){
PushController.validatePushType(where, validPushTypes);
validatePushType(where, validPushTypes);
}).not.toThrow();
done();
});
@@ -63,7 +64,7 @@ describe('PushController', () => {
var validPushTypes = ['ios', 'android'];
expect(function(){
PushController.validatePushType(where, validPushTypes);
validatePushType(where, validPushTypes);
}).not.toThrow();
done();
});
@@ -76,7 +77,7 @@ describe('PushController', () => {
var validPushTypes = ['ios', 'android'];
expect(function(){
PushController.validatePushType(where, validPushTypes);
validatePushType(where, validPushTypes);
}).toThrow();
done();
});
@@ -89,7 +90,7 @@ describe('PushController', () => {
var validPushTypes = ['ios', 'android'];
expect(function(){
PushController.validatePushType(where, validPushTypes);
validatePushType(where, validPushTypes);
}).toThrow();
done();
});
@@ -98,8 +99,8 @@ describe('PushController', () => {
// Make mock request
var timeStr = '2015-03-19T22:05:08Z';
var body = {
'expiration_time': timeStr
}
'expiration_time': timeStr
}
var time = PushController.getExpirationTime(body);
expect(time).toEqual(new Date(timeStr).valueOf());
@@ -131,122 +132,32 @@ describe('PushController', () => {
});
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 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 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)
const 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");
@@ -254,50 +165,220 @@ describe('PushController', () => {
}
while(installations.length != 15) {
var installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_"+installations.length);
installation.set("deviceToken","device_token_"+installations.length)
const 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 config = new Config(Parse.applicationId);
var auth = {
isMaster: true
}
var pushController = new PushController();
reconfigureServer({
push: { adapter: pushAdapter }
}).then(() => {
return Parse.Object.saveAll(installations)
}).then(() => {
return pushController.sendPush(payload, {}, config, auth);
}).then(() => {
done();
}, (err) => {
jfail(err);
done();
});
});
var pushController = new PushController(pushAdapter, Parse.applicationId);
Parse.Object.saveAll(installations).then(() => {
it('properly set badges to 1', (done) => {
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 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 config = new Config(Parse.applicationId);
var auth = {
isMaster: true
}
var pushController = new PushController();
reconfigureServer({
push: { adapter: pushAdapter }
}).then(() => {
return Parse.Object.saveAll(installations)
}).then(() => {
return pushController.sendPush(payload, {}, config, auth);
}).then(() => {
done();
}, () => {
fail("should not fail");
done();
});
});
it('properly set badges to 1 with complex query #2903 #3022', (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);
}
let matchedInstallationsCount = 0;
var pushAdapter = {
send: function(body, installations) {
matchedInstallationsCount += installations.length;
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();
reconfigureServer({
push: {
adapter: pushAdapter
}
}).then(() => {
return Parse.Object.saveAll(installations)
}).then((installations) => {
const objectIds = installations.map(installation => {
return installation.id;
})
const where = {
objectId: {'$in': objectIds.slice(0, 5)}
}
return pushController.sendPush(payload, where, config, auth);
}).then(() => {
return new Promise((res) => {
setTimeout(res, 300);
});
}).then(() => {
expect(matchedInstallationsCount).toBe(5);
const query = new Parse.Query(Parse.Installation);
query.equalTo('badge', 1);
return query.find({useMasterKey: true});
}).then((installations) => {
expect(installations.length).toBe(5);
done();
}).catch(() => {
fail("should not fail");
done();
});
});
it('properly creates _PushStatus', (done) => {
var installations = [];
while(installations.length != 10) {
const 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) {
const 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();
reconfigureServer({
push: { adapter: pushAdapter }
}).then(() => {
return Parse.Object.saveAll(installations)
})
.then(() => {
return pushController.sendPush(payload, {}, config, auth);
}).then((result) => {
return new Promise((resolve, reject) => {
}).then(() => {
// it is enqueued so it can take time
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 1000);
});
}).then(() => {
let query = new Parse.Query('_PushStatus');
}).then(() => {
const query = new Parse.Query('_PushStatus');
return query.find({useMasterKey: true});
}).then((results) => {
expect(results.length).toBe(1);
let result = results[0];
const result = results[0];
expect(result.createdAt instanceof Date).toBe(true);
expect(result.updatedAt instanceof Date).toBe(true);
expect(result.id.length).toBe(10);
expect(result.get('source')).toEqual('rest');
expect(result.get('query')).toEqual(JSON.stringify({}));
expect(result.get('payload')).toEqual(payload.data);
expect(typeof result.get('payload')).toEqual("string");
expect(JSON.parse(result.get('payload'))).toEqual(payload.data);
expect(result.get('status')).toEqual('succeeded');
expect(result.get('numSent')).toEqual(10);
expect(result.get('sentPerType')).toEqual({
@@ -308,57 +389,147 @@ describe('PushController', () => {
'android': 5 // android
});
// Try to get it without masterKey
let query = new Parse.Query('_PushStatus');
const query = new Parse.Query('_PushStatus');
return query.find();
}).then((results) => {
expect(results.length).toBe(0);
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"];
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"];
}
}
}
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) => {
const 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();
reconfigureServer({
push: { adapter: pushAdapter }
}).then(() => {
return pushController.sendPush(payload, where, config, auth)
}).then(() => {
fail('should not succeed');
done();
}).catch((err) => {
}).catch(() => {
const query = new Parse.Query('_PushStatus');
query.find({useMasterKey: true}).then((results) => {
expect(results.length).toBe(1);
const 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
}
const where = {
'deviceToken': {
'$inQuery': {
'where': {
'deviceType': 'ios'
},
className: '_Installation'
}
}
}
var pushController = new PushController();
reconfigureServer({
push: { adapter: pushAdapter }
}).then(() => {
return pushController.sendPush(payload, where, config, auth);
}).then(() => {
done();
}).catch((err) => {
jfail(err);
done();
});
});
it('should support object type for alert', (done) => {
var payload = {data: {
alert: {
'loc-key': 'hello_world',
},
}}
var pushAdapter = {
send: function(body, installations) {
return successfulTransmissions(body, installations);
},
getValidPushTypes: function() {
return ["ios"];
}
}
var config = new Config(Parse.applicationId);
var auth = {
isMaster: true
}
const where = {
'deviceToken': {
'$inQuery': {
'where': {
'deviceType': 'ios'
},
className: '_Installation'
}
}
}
var pushController = new PushController();
reconfigureServer({
push: { adapter: pushAdapter }
}).then(() => {
pushController.sendPush(payload, where, config, auth)
}).then(() => {
done();
}).catch(() => {
fail('should not fail');
done();
});
});
it('should flatten', () => {
var res = pushStatusHandler.flatten([1, [2], [[3, 4], 5], [[[6]]]])
var res = StatusHandler.flatten([1, [2], [[3, 4], 5], [[[6]]]])
expect(res).toEqual([1,2,3,4,5,6]);
})
});
+6 -3
View File
@@ -67,10 +67,10 @@ describe('PushRouter', () => {
}).toThrow();
done();
});
it('sends a push through REST', (done) => {
request.post({
url: Parse.serverURL+"/push",
url: Parse.serverURL + "/push",
json: true,
body: {
'channels': {
@@ -82,8 +82,11 @@ describe('PushRouter', () => {
'X-Parse-Master-Key': Parse.masterKey
}
}, function(err, res, body){
expect(res.headers['x-parse-push-status-id']).not.toBe(undefined);
expect(res.headers['x-parse-push-status-id'].length).toBe(10);
expect(res.headers[''])
expect(body.result).toBe(true);
done();
});
});
});
});
+57
View File
@@ -0,0 +1,57 @@
var PushWorker = require('../src').PushWorker;
var Config = require('../src/Config');
describe('PushWorker', () => {
it('should run with small batch', (done) => {
const batchSize = 3;
var sendCount = 0;
reconfigureServer({
push: {
queueOptions: {
disablePushWorker: true,
batchSize
}
}
}).then(() => {
expect(new Config('test').pushWorker).toBeUndefined();
new PushWorker({
send: (body, installations) => {
expect(installations.length <= batchSize).toBe(true);
sendCount += installations.length;
return Promise.resolve();
},
getValidPushTypes: function() {
return ['ios', 'android']
}
});
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", 1);
installation.set("deviceType", "ios");
installations.push(installation);
}
return Parse.Object.saveAll(installations);
}).then(() => {
return Parse.Push.send({
where: {
deviceType: 'ios'
},
data: {
alert: 'Hello world!'
}
}, {useMasterKey: true})
}).then(() => {
return new Promise((resolve) => {
setTimeout(resolve, 500);
});
}).then(() => {
expect(sendCount).toBe(10);
done();
}).catch(err => {
jfail(err);
})
});
});
+93
View File
@@ -112,6 +112,20 @@ describe('matchesQuery', function() {
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({
@@ -212,6 +226,23 @@ describe('matchesQuery', function() {
img.owner.objectId = 'U3';
expect(matchesQuery(img, q)).toBe(false);
// pointers in arrays
q = new Parse.Query('Image');
q.equalTo('owners', u);
img = {
className: 'Image',
objectId: 'I1',
owners: [{
className: '_User',
objectId: 'U2'
}]
};
expect(matchesQuery(img, q)).toBe(true);
img.owners[0].objectId = 'U3';
expect(matchesQuery(img, q)).toBe(false);
});
it('matches on inequalities', function() {
@@ -381,4 +412,66 @@ describe('matchesQuery', function() {
expect(matchesQuery(caltrainStation, q)).toBe(false);
expect(matchesQuery(santaClara, q)).toBe(false);
});
it('matches on subobjects with dot notation', function() {
var message = {
id: new Id('Message', 'O1'),
text: "content",
status: {x: "read", y: "delivered"}
};
var q = new Parse.Query('Message');
q.equalTo("status.x", "read");
expect(matchesQuery(message, q)).toBe(true);
q = new Parse.Query('Message');
q.equalTo("status.z", "read");
expect(matchesQuery(message, q)).toBe(false);
q = new Parse.Query('Message');
q.equalTo("status.x", "delivered");
expect(matchesQuery(message, q)).toBe(false);
q = new Parse.Query('Message');
q.notEqualTo("status.x", "read");
expect(matchesQuery(message, q)).toBe(false);
q = new Parse.Query('Message');
q.notEqualTo("status.z", "read");
expect(matchesQuery(message, q)).toBe(true);
q = new Parse.Query('Message');
q.notEqualTo("status.x", "delivered");
expect(matchesQuery(message, q)).toBe(true);
q = new Parse.Query('Message');
q.exists("status.x");
expect(matchesQuery(message, q)).toBe(true);
q = new Parse.Query('Message');
q.exists("status.z");
expect(matchesQuery(message, q)).toBe(false);
q = new Parse.Query('Message');
q.exists("nonexistent.x");
expect(matchesQuery(message, q)).toBe(false);
q = new Parse.Query('Message');
q.doesNotExist("status.x");
expect(matchesQuery(message, q)).toBe(false);
q = new Parse.Query('Message');
q.doesNotExist("status.z");
expect(matchesQuery(message, q)).toBe(true);
q = new Parse.Query('Message');
q.doesNotExist("nonexistent.z");
expect(matchesQuery(message, q)).toBe(true);
q = new Parse.Query('Message');
q.equalTo("status.x", "read");
q.doesNotExist("status.y");
expect(matchesQuery(message, q)).toBe(false);
});
});
+50
View File
@@ -0,0 +1,50 @@
var RedisCacheAdapter = require('../src/Adapters/Cache/RedisCacheAdapter').default;
describe('RedisCacheAdapter', function() {
var KEY = 'hello';
var VALUE = 'world';
function wait(sleep) {
return new Promise(function(resolve) {
setTimeout(resolve, sleep);
})
}
it('should get/set/clear', (done) => {
var cache = new RedisCacheAdapter({
ttl: NaN
});
cache.put(KEY, VALUE)
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(VALUE))
.then(() => cache.clear())
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(null))
.then(done);
});
it('should expire after ttl', (done) => {
var cache = new RedisCacheAdapter(null, 1);
cache.put(KEY, VALUE)
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(VALUE))
.then(wait.bind(null, 2))
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(null))
.then(done);
});
it('should find un-expired records', (done) => {
var cache = new RedisCacheAdapter(null, 5);
cache.put(KEY, VALUE)
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(VALUE))
.then(wait.bind(null, 1))
.then(() => cache.get(KEY))
.then((value) => expect(value).not.toEqual(null))
.then(done);
});
});
+3 -3
View File
@@ -1,4 +1,4 @@
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
var RedisPubSub = require('../src/Adapters/PubSub/RedisPubSub').RedisPubSub;
describe('RedisPubSub', function() {
@@ -10,14 +10,14 @@ describe('RedisPubSub', function() {
});
it('can create publisher', function() {
var publisher = RedisPubSub.createPublisher('redisAddress');
RedisPubSub.createPublisher({redisURL: 'redisAddress'});
var redis = require('redis');
expect(redis.createClient).toHaveBeenCalledWith('redisAddress', { no_ready_check: true });
});
it('can create subscriber', function() {
var subscriber = RedisPubSub.createSubscriber('redisAddress');
RedisPubSub.createSubscriber({redisURL: 'redisAddress'});
var redis = require('redis');
expect(redis.createClient).toHaveBeenCalledWith('redisAddress', { no_ready_check: true });
-287
View File
@@ -1,287 +0,0 @@
// 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');
var DatabaseAdapter = require('../src/DatabaseAdapter');
var Parse = require('parse/node').Parse;
var rest = require('../src/rest');
var request = require('request');
var config = new Config('test');
var database = DatabaseAdapter.getDatabaseConnection('test', 'test_');
describe('rest create', () => {
it('handles _id', (done) => {
rest.create(config, auth.nobody(config), 'Foo', {}).then(() => {
return database.mongoFind('Foo', {});
}).then((results) => {
expect(results.length).toEqual(1);
var obj = results[0];
expect(typeof obj._id).toEqual('string');
expect(obj.objectId).toBeUndefined();
done();
});
});
it('handles array, object, date', (done) => {
var obj = {
array: [1, 2, 3],
object: {foo: 'bar'},
date: Parse._encode(new Date()),
};
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(mob.array instanceof Array).toBe(true);
expect(typeof mob.object).toBe('object');
expect(mob.date instanceof Date).toBe(true);
done();
});
});
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',
password: 'zxcv',
foo: 'bar',
};
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');
done();
});
});
it('handles anonymous user signup', (done) => {
var data1 = {
authData: {
anonymous: {
id: '00000000-0000-0000-0000-000000000001'
}
}
};
var data2 = {
authData: {
anonymous: {
id: '00000000-0000-0000-0000-000000000002'
}
}
};
var username1;
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');
return 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.username).toEqual('string');
expect(typeof r.response.updatedAt).toEqual('string');
username1 = r.response.username;
return rest.create(config, auth.nobody(config), '_User', data2);
}).then((r) => {
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.sessionToken).toEqual('string');
return rest.create(config, auth.nobody(config), '_User', data2);
}).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.username).not.toEqual(username1);
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);
NoAnnonConfig.authDataManager.setEnableAnonymousUsers(false);
var data1 = {
authData: {
anonymous: {
id: '00000000-0000-0000-0000-000000000001'
}
}
};
rest.create(NoAnnonConfig, auth.nobody(NoAnnonConfig), '_User', data1).then(() => {
fail("Should throw an error");
done();
}, (err) => {
expect(err.code).toEqual(Parse.Error.UNSUPPORTED_SERVICE);
expect(err.message).toEqual('This authentication method is unsupported.');
NoAnnonConfig.authDataManager.setEnableAnonymousUsers(true);
done();
})
});
it('test facebook signup and login', (done) => {
var data = {
authData: {
facebook: {
id: '8675309',
access_token: 'jenny'
}
}
};
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();
});
});
it('stores pointers with a _p_ prefix', (done) => {
var obj = {
foo: 'bar',
aPointer: {
__type: 'Pointer',
className: 'JustThePointer',
objectId: 'qwerty'
}
};
rest.create(config, auth.nobody(config), 'APointerDarkly', obj)
.then((r) => {
return database.mongoFind('APointerDarkly', {});
}).then((results) => {
expect(results.length).toEqual(1);
var output = results[0];
expect(typeof output._id).toEqual('string');
expect(typeof output._p_aPointer).toEqual('string');
expect(output._p_aPointer).toEqual('JustThePointer$qwerty');
expect(output.aPointer).toBeUndefined();
done();
});
});
it("cannot set objectId", (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/classes/TestObject',
body: JSON.stringify({
'foo': 'bar',
'objectId': 'hello'
})
}, (error, response, body) => {
var b = JSON.parse(body);
expect(b.code).toEqual(105);
expect(b.error).toEqual('objectId is an invalid field name.');
done();
});
});
});
+140 -29
View File
@@ -1,16 +1,23 @@
'use strict'
// These tests check the "find" functionality of the REST API.
var auth = require('../src/Auth');
var cache = require('../src/cache');
var Config = require('../src/Config');
var rest = require('../src/rest');
var querystring = require('querystring');
var request = require('request');
var rp = require('request-promise');
var config = new Config('test');
var config;
let database;
var nobody = auth.nobody(config);
describe('rest query', () => {
beforeEach(() => {
config = new Config('test');
database = config.database;
});
it('basic query', (done) => {
rest.create(config, nobody, 'TestObject', {}).then(() => {
return rest.find(config, nobody, 'TestObject', {});
@@ -35,8 +42,38 @@ describe('rest query', () => {
});
});
var data = {
username: 'blah',
password: 'pass',
sessionToken: 'abc123',
}
it_exclude_dbs(['postgres'])('query for user w/ legacy credentials without masterKey has them stripped from results', done => {
database.create('_User', data).then(() => {
return rest.find(config, nobody, '_User')
}).then((result) => {
var user = result.results[0];
expect(user.username).toEqual('blah');
expect(user.sessionToken).toBeUndefined();
expect(user.password).toBeUndefined();
done();
});
});
it_exclude_dbs(['postgres'])('query for user w/ legacy credentials with masterKey has them stripped from results', done => {
database.create('_User', data).then(() => {
return rest.find(config, {isMaster: true}, '_User')
}).then((result) => {
var user = result.results[0];
expect(user.username).toEqual('blah');
expect(user.sessionToken).toBeUndefined();
expect(user.password).toBeUndefined();
done();
});
});
// Created to test a scenario in AnyPic
it('query with include', (done) => {
it_exclude_dbs(['postgres'])('query with include', (done) => {
var photo = {
foo: 'bar'
};
@@ -106,6 +143,22 @@ describe('rest query', () => {
expect(err.message).toEqual('This user is not allowed to access ' +
'non-existent class: ClientClassCreation');
done();
});
});
it('query existent class when disabled client class creation', (done) => {
var customConfig = Object.assign({}, config, {allowClientClassCreation: false});
config.database.loadSchema()
.then(schema => schema.addClassIfNotExists('ClientClassCreation', {}))
.then(actualSchema => {
expect(actualSchema.className).toEqual('ClientClassCreation');
return rest.find(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {});
})
.then((result) => {
expect(result.results.length).toEqual(0);
done();
}, () => {
fail('Should not throw error')
});
});
@@ -119,39 +172,97 @@ describe('rest query', () => {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'
};
request.get({
const p0 = rp.get({
headers: headers,
url: 'http://localhost:8378/1/classes/TestParameterEncode?'
+ querystring.stringify({
where: '{"foo":{"$ne": "baz"}}',
limit: 1
where: '{"foo":{"$ne": "baz"}}',
limit: 1
}).replace('=', '%3D'),
}, (error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
}).then(fail, (response) => {
const error = response.error;
var b = JSON.parse(error);
expect(b.code).toEqual(Parse.Error.INVALID_QUERY);
expect(b.error).toEqual('Improper encode of parameter');
done();
});
const p1 = rp.get({
headers: headers,
url: 'http://localhost:8378/1/classes/TestParameterEncode?'
+ querystring.stringify({
limit: 1
}).replace('=', '%3D'),
}).then(fail, (response) => {
const error = response.error;
var b = JSON.parse(error);
expect(b.code).toEqual(Parse.Error.INVALID_QUERY);
});
return Promise.all([p0, p1]);
}).then(done).catch((err) => {
jfail(err);
fail('should not fail');
done();
})
});
it('query with limit = 0', (done) => {
rest.create(config, nobody, 'TestObject', {foo: 'baz'}
).then(() => {
return rest.create(config, nobody,
'TestObject', {foo: 'qux'});
}).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();
});
return rest.find(config, nobody,
'TestObject', {}, {limit: 0});
}).then((response) => {
expect(response.results.length).toEqual(0);
done();
});
});
it('query with limit = 0 and count = 1', (done) => {
rest.create(config, nobody, 'TestObject', {foo: 'baz'}
).then(() => {
return rest.create(config, nobody,
'TestObject', {foo: 'qux'});
}).then(() => {
return rest.find(config, nobody,
'TestObject', {}, {limit: 0, count: 1});
}).then((response) => {
expect(response.results.length).toEqual(0);
expect(response.count).toEqual(2);
done();
});
});
it('makes sure null pointers are handed correctly #2189', done => {
const object = new Parse.Object('AnObject');
const anotherObject = new Parse.Object('AnotherObject');
anotherObject.save().then(() => {
object.set('values', [null, null, anotherObject]);
return object.save();
}).then(() => {
const query = new Parse.Query('AnObject');
query.include('values');
return query.first();
}).then((result) => {
const values = result.get('values');
expect(values.length).toBe(3);
let anotherObjectFound = false;
let nullCounts = 0;
for(const value of values) {
if (value === null) {
nullCounts++;
} else if (value instanceof Parse.Object) {
anotherObjectFound = true;
}
}
expect(nullCounts).toBe(2);
expect(anotherObjectFound).toBeTruthy();
done();
}, (err) => {
console.error(err);
fail(err);
done();
});
});
});
+112
View File
@@ -0,0 +1,112 @@
const Config = require('../src/Config');
const sessionToken = 'legacySessionToken';
const rp = require('request-promise');
const Parse = require('parse/node');
function createUser() {
const config = new Config(Parse.applicationId);
const user = {
objectId: '1234567890',
username: 'hello',
password: 'pass',
_session_token: sessionToken
}
return config.database.create('_User', user);
}
describe_only_db('mongo')('revocable sessions', () => {
beforeEach((done) => {
// Create 1 user with the legacy
createUser().then(done);
});
it('should upgrade legacy session token', done => {
const user = Parse.Object.fromJSON({
className: '_User',
objectId: '1234567890',
sessionToken: sessionToken
});
user._upgradeToRevocableSession().then((res) => {
expect(res.getSessionToken().indexOf('r:')).toBe(0);
const config = new Config(Parse.applicationId);
// use direct access to the DB to make sure we're not
// getting the session token stripped
return config.database.loadSchema().then(schemaController => {
return schemaController.getOneSchema('_User', true)
}).then((schema) => {
return config.database.adapter.find('_User', schema, {objectId: '1234567890'}, {})
}).then((results) => {
expect(results.length).toBe(1);
expect(results[0].sessionToken).toBeUndefined();
});
}).then(() => {
done();
}, (err) => {
jfail(err);
done();
});
});
it('should be able to become with revocable session token', done => {
const user = Parse.Object.fromJSON({
className: '_User',
objectId: '1234567890',
sessionToken: sessionToken
});
user._upgradeToRevocableSession().then((res) => {
expect(res.getSessionToken().indexOf('r:')).toBe(0);
return Parse.User.logOut().then(() => {
return Parse.User.become(res.getSessionToken())
}).then((user) => {
expect(user.id).toEqual('1234567890');
});
}).then(() => {
done();
}, (err) => {
jfail(err);
done();
});
});
it('should not upgrade bad legacy session token', done => {
rp.post({
url: Parse.serverURL + '/upgradeToRevocableSession',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Rest-API-Key': 'rest',
'X-Parse-Session-Token': 'badSessionToken'
},
json: true
}).then(() => {
fail('should not be able to upgrade a bad token');
}, (response) => {
expect(response.statusCode).toBe(400);
expect(response.error).not.toBeUndefined();
expect(response.error.code).toBe(Parse.Error.INVALID_SESSION_TOKEN);
expect(response.error.error).toEqual('invalid legacy session token');
}).then(() => {
done();
});
});
it('should not crash without session token #2720', done => {
rp.post({
url: Parse.serverURL + '/upgradeToRevocableSession',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Rest-API-Key': 'rest'
},
json: true
}).then(() => {
fail('should not be able to upgrade a bad token');
}, (response) => {
expect(response.statusCode).toBe(404);
expect(response.error).not.toBeUndefined();
expect(response.error.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
expect(response.error.error).toEqual('invalid session');
}).then(() => {
done();
});
});
})
+584 -176
View File
@@ -1,10 +1,10 @@
'use strict';
var Config = require('../src/Config');
var Schema = require('../src/Schema');
var SchemaController = require('../src/Controllers/SchemaController');
var dd = require('deep-diff');
var config = new Config('test');
var config;
var hasAllPODobject = () => {
var obj = new Parse.Object('HasAllPOD');
@@ -19,14 +19,18 @@ var hasAllPODobject = () => {
return obj;
};
describe('Schema', () => {
describe('SchemaController', () => {
beforeEach(() => {
config = new Config('test');
});
it('can validate one object', (done) => {
config.database.loadSchema().then((schema) => {
return schema.validateObject('TestObject', {a: 1, b: 'yo', c: false});
}).then((schema) => {
}).then(() => {
done();
}, (error) => {
fail(error);
jfail(error);
done();
});
});
@@ -34,10 +38,10 @@ 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) => {
}).then(() => {
done();
}, (error) => {
fail(error);
jfail(error);
done();
});
});
@@ -47,7 +51,22 @@ describe('Schema', () => {
return schema.validateObject('Foo', {x: true, y: 'yyy', z: 0});
}).then((schema) => {
return schema.validateObject('Foo', {x: false, y: 'YY', z: 1});
}).then(() => {
done();
});
});
it('can validate Relation object', (done) => {
config.database.loadSchema().then((schema) => {
return schema.validateObject('Stuff', {aRelation: {__type:'Relation',className:'Stuff'}});
}).then((schema) => {
return schema.validateObject('Stuff', {aRelation: {__type:'Pointer',className:'Stuff'}})
.then(() => {
fail('expected invalidity');
done();
}, done);
}, (err) => {
fail(err);
done();
});
});
@@ -84,13 +103,13 @@ describe('Schema', () => {
return schema.setPermissions('Stuff', {
'find': {}
});
}).then((schema) => {
}).then(() => {
var query = new Parse.Query('Stuff');
return query.find();
}).then((results) => {
}).then(() => {
fail('Class permissions should have rejected this query.');
done();
}, (e) => {
}, () => {
done();
});
});
@@ -109,51 +128,52 @@ describe('Schema', () => {
return schema.setPermissions('Stuff', {
'find': find
});
}).then((schema) => {
}).then(() => {
var query = new Parse.Query('Stuff');
return query.find();
}).then((results) => {
}).then(() => {
done();
}, (e) => {
}, () => {
fail('Class permissions should have allowed this query.');
done();
});
});
it('class-level permissions test get', (done) => {
var user;
var obj;
createTestUser().then((u) => {
user = u;
return config.database.loadSchema();
}).then((schema) => {
// Just to create a valid class
return schema.validateObject('Stuff', {foo: 'bar'});
}).then((schema) => {
var find = {};
var get = {};
get[user.id] = true;
return schema.setPermissions('Stuff', {
'find': find,
'get': get
});
}).then((schema) => {
obj = new Parse.Object('Stuff');
obj.set('foo', 'bar');
return obj.save();
}).then((o) => {
obj = o;
var query = new Parse.Query('Stuff');
return query.find();
}).then((results) => {
fail('Class permissions should have rejected this query.');
done();
}, (e) => {
var query = new Parse.Query('Stuff');
return query.get(obj.id).then((o) => {
createTestUser()
.then(user => {
return config.database.loadSchema()
// Create a valid class
.then(schema => schema.validateObject('Stuff', {foo: 'bar'}))
.then(schema => {
var find = {};
var get = {};
get[user.id] = true;
return schema.setPermissions('Stuff', {
'create': {'*': true},
'find': find,
'get': get
});
}).then(() => {
obj = new Parse.Object('Stuff');
obj.set('foo', 'bar');
return obj.save();
}).then((o) => {
obj = o;
var query = new Parse.Query('Stuff');
return query.find();
}).then(() => {
fail('Class permissions should have rejected this query.');
done();
}, (e) => {
fail('Class permissions should have allowed this get query');
}, () => {
var query = new Parse.Query('Stuff');
return query.get(obj.id).then(() => {
done();
}, () => {
fail('Class permissions should have allowed this get query');
done();
});
});
});
});
@@ -163,14 +183,26 @@ describe('Schema', () => {
.then(schema => schema.addClassIfNotExists('NewClass', {
foo: {type: 'String'}
}))
.then(result => {
expect(result).toEqual({
_id: 'NewClass',
objectId: 'string',
updatedAt: 'string',
createdAt: 'string',
foo: 'string',
})
.then(actualSchema => {
const expectedSchema = {
className: 'NewClass',
fields: {
objectId: { type: 'String' },
updatedAt: { type: 'Date' },
createdAt: { type: 'Date' },
ACL: { type: 'ACL' },
foo: { type: 'String' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
},
}
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
done();
})
.catch(error => {
@@ -201,15 +233,27 @@ describe('Schema', () => {
.then(schema => {
var p1 = schema.addClassIfNotExists('NewClass', {foo: {type: 'String'}});
var p2 = schema.addClassIfNotExists('NewClass', {foo: {type: 'String'}});
Promise.race([p1, p2]) //Use race because we expect the first completed promise to be the successful one
.then(response => {
expect(response).toEqual({
_id: 'NewClass',
objectId: 'string',
updatedAt: 'string',
createdAt: 'string',
foo: 'string',
});
Promise.race([p1, p2])
.then(actualSchema => {
const expectedSchema = {
className: 'NewClass',
fields: {
objectId: { type: 'String' },
updatedAt: { type: 'Date' },
createdAt: { type: 'Date' },
ACL: { type: 'ACL' },
foo: { type: 'String' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
},
}
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
});
Promise.all([p1,p2])
.catch(error => {
@@ -373,23 +417,35 @@ describe('Schema', () => {
aPointer: {type: 'Pointer', targetClass: 'ThisClassDoesNotExistYet'},
aRelation: {type: 'Relation', targetClass: 'NewClass'},
}))
.then(mongoObj => {
expect(mongoObj).toEqual({
_id: 'NewClass',
objectId: 'string',
createdAt: 'string',
updatedAt: 'string',
aNumber: 'number',
aString: 'string',
aBool: 'boolean',
aDate: 'date',
aObject: 'object',
aArray: 'array',
aGeoPoint: 'geopoint',
aFile: 'file',
aPointer: '*ThisClassDoesNotExistYet',
aRelation: 'relation<NewClass>',
});
.then(actualSchema => {
const expectedSchema = {
className: 'NewClass',
fields: {
objectId: { type: 'String' },
updatedAt: { type: 'Date' },
createdAt: { type: 'Date' },
ACL: { type: 'ACL' },
aString: { type: 'String' },
aNumber: { type: 'Number' },
aBool: { type: 'Boolean' },
aDate: { type: 'Date' },
aObject: { type: 'Object' },
aArray: { type: 'Array' },
aGeoPoint: { type: 'GeoPoint' },
aFile: { type: 'File' },
aPointer: { type: 'Pointer', targetClass: 'ThisClassDoesNotExistYet' },
aRelation: { type: 'Relation', targetClass: 'NewClass' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
},
}
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
done();
});
});
@@ -399,40 +455,69 @@ describe('Schema', () => {
.then(schema => schema.addClassIfNotExists('_Installation', {
foo: {type: 'Number'},
}))
.then(mongoObj => {
expect(mongoObj).toEqual({
_id: '_Installation',
createdAt: 'string',
updatedAt: 'string',
objectId: 'string',
foo: 'number',
installationId: 'string',
deviceToken: 'string',
channels: 'array',
deviceType: 'string',
pushType: 'string',
GCMSenderId: 'string',
timeZone: 'string',
localeIdentifier: 'string',
badge: 'number',
});
.then(actualSchema => {
const expectedSchema = {
className: '_Installation',
fields: {
objectId: { type: 'String' },
updatedAt: { type: 'Date' },
createdAt: { type: 'Date' },
ACL: { type: 'ACL' },
foo: { type: 'Number' },
installationId: { type: 'String' },
deviceToken: { type: 'String' },
channels: { type: 'Array' },
deviceType: { type: 'String' },
pushType: { type: 'String' },
GCMSenderId: { type: 'String' },
timeZone: { type: 'String' },
localeIdentifier: { type: 'String' },
badge: { type: 'Number' },
appVersion: { type: 'String' },
appName: { type: 'String' },
appIdentifier: { type: 'String' },
parseVersion: { type: 'String' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
},
}
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
done();
});
});
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>',
});
//as `_Role` is always created by default, we only get it here
.then(schema => schema.getOneSchema('_Role'))
.then(actualSchema => {
const expectedSchema = {
className: '_Role',
fields: {
objectId: { type: 'String' },
updatedAt: { type: 'Date' },
createdAt: { type: 'Date' },
ACL: { type: 'ACL' },
name: { type: 'String' },
users: { type: 'Relation', targetClass: '_User' },
roles: { type: 'Relation', targetClass: '_Role' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
},
};
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
done();
});
});
@@ -440,19 +525,31 @@ describe('Schema', () => {
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'
});
.then(actualSchema => {
const expectedSchema = {
className: '_Session',
fields: {
objectId: { type: 'String' },
updatedAt: { type: 'Date' },
createdAt: { type: 'Date' },
restricted: { type: 'Boolean' },
user: { type: 'Pointer', targetClass: '_User' },
installationId: { type: 'String' },
sessionToken: { type: 'String' },
expiresAt: { type: 'Date' },
createdWith: { type: 'Object' },
ACL: { type: 'ACL' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
},
};
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
done();
});
});
@@ -491,10 +588,10 @@ describe('Schema', () => {
})
.catch(error => {
fail('Couldn\'t create class');
fail(error);
jfail(error);
});
})
.catch(error => fail('Couldn\'t load schema'));
.catch(() => fail('Couldn\'t load schema'));
});
it('refuses to delete fields from invalid class names', done => {
@@ -572,7 +669,7 @@ describe('Schema', () => {
}
done();
}, error => {
fail(error);
jfail(error);
done();
});
});
@@ -583,27 +680,47 @@ describe('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(actualSchema => {
const expectedSchema = {
className: 'NewClass',
fields: {
objectId: { type: 'String' },
updatedAt: { type: 'Date' },
createdAt: { type: 'Date' },
ACL: { type: 'ACL' },
relationField: { type: 'Relation', targetClass: '_User' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
},
};
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
})
.then(() => config.database.collectionExists('_Join:relationField:NewClass'))
.then(exist => {
expect(exist).toEqual(false);
on_db('postgres', () => {
// We create the table when creating the column
expect(exist).toEqual(true);
}, () => {
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'
});
const expectedSchema = {
objectId: { type: 'String' },
updatedAt: { type: 'Date' },
createdAt: { type: 'Date' },
ACL: { type: 'ACL' },
};
expect(dd(schema.data.NewClass, expectedSchema)).toEqual(undefined);
done();
});
});
@@ -613,7 +730,7 @@ describe('Schema', () => {
Parse.Object.disableSingleInstance();
var obj1 = hasAllPODobject();
var obj2 = hasAllPODobject();
var p = Parse.Object.saveAll([obj1, obj2])
Parse.Object.saveAll([obj1, obj2])
.then(() => config.database.loadSchema())
.then(schema => schema.deleteField('aString', 'HasAllPOD', config.database))
.then(() => new Parse.Query('HasAllPOD').get(obj1.id))
@@ -630,6 +747,10 @@ describe('Schema', () => {
done();
Parse.Object.enableSingleInstance();
});
})
.catch(error => {
jfail(error);
done();
});
});
@@ -660,9 +781,9 @@ describe('Schema', () => {
});
it('can merge schemas', done => {
expect(Schema.buildMergedSchemaObject({
expect(SchemaController.buildMergedSchemaObject({
_id: 'SomeClass',
someType: 'number'
someType: { type: 'Number' }
}, {
newType: {type: 'Number'}
})).toEqual({
@@ -673,10 +794,10 @@ describe('Schema', () => {
});
it('can merge deletions', done => {
expect(Schema.buildMergedSchemaObject({
expect(SchemaController.buildMergedSchemaObject({
_id: 'SomeClass',
someType: 'number',
outDatedType: 'string',
someType: { type: 'Number' },
outDatedType: { type: 'String' },
},{
newType: {type: 'GeoPoint'},
outDatedType: {__op: 'Delete'},
@@ -688,48 +809,335 @@ describe('Schema', () => {
});
it('ignore default field when merge with system class', done => {
expect(Schema.buildMergedSchemaObject({
expect(SchemaController.buildMergedSchemaObject({
_id: '_User',
username: 'string',
password: 'string',
authData: 'object',
email: 'string',
emailVerified: 'boolean'
username: { type: 'String' },
password: { type: 'String' },
email: { type: 'String' },
emailVerified: { type: 'Boolean' },
},{
authData: {type: 'string'},
customField: {type: 'string'},
emailVerified: { type: 'String' },
customField: { type: 'String' },
})).toEqual({
customField: {type: 'string'}
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},
it('yields a proper schema mismatch error (#2661)', done => {
const anObject = new Parse.Object('AnObject');
const anotherObject = new Parse.Object('AnotherObject');
const someObject = new Parse.Object('SomeObject');
Parse.Object.saveAll([anObject, anotherObject, someObject]).then(() => {
anObject.set('pointer', anotherObject);
return anObject.save();
}).then(() => {
anObject.set('pointer', someObject);
return anObject.save();
}).then(() => {
fail('shoud not save correctly');
done();
}, (err) => {
expect(err instanceof Parse.Error).toBeTruthy();
expect(err.message).toEqual('schema mismatch for AnObject.pointer; expected Pointer<AnotherObject> but got Pointer<SomeObject>')
done();
});
});
it('yields a proper schema mismatch error bis (#2661)', done => {
const anObject = new Parse.Object('AnObject');
const someObject = new Parse.Object('SomeObject');
Parse.Object.saveAll([anObject, someObject]).then(() => {
anObject.set('number', 1);
return anObject.save();
}).then(() => {
anObject.set('number', someObject);
return anObject.save();
}).then(() => {
fail('shoud not save correctly');
done();
}, (err) => {
expect(err instanceof Parse.Error).toBeTruthy();
expect(err.message).toEqual('schema mismatch for AnObject.number; expected Number but got Pointer<SomeObject>')
done();
});
});
it('yields a proper schema mismatch error ter (#2661)', done => {
const anObject = new Parse.Object('AnObject');
const someObject = new Parse.Object('SomeObject');
Parse.Object.saveAll([anObject, someObject]).then(() => {
anObject.set('pointer', someObject);
return anObject.save();
}).then(() => {
anObject.set('pointer', 1);
return anObject.save();
}).then(() => {
fail('shoud not save correctly');
done();
}, (err) => {
expect(err instanceof Parse.Error).toBeTruthy();
expect(err.message).toEqual('schema mismatch for AnObject.pointer; expected Pointer<SomeObject> but got Number')
done();
});
});
it('properly handles volatile _Schemas', done => {
function validateSchemaStructure(schema) {
expect(schema.hasOwnProperty('className')).toBe(true);
expect(schema.hasOwnProperty('fields')).toBe(true);
expect(schema.hasOwnProperty('classLevelPermissions')).toBe(true);
}
function validateSchemaDataStructure(schemaData) {
Object.keys(schemaData).forEach(className => {
const schema = schemaData[className];
// Hooks has className...
if (className != '_Hooks') {
expect(schema.hasOwnProperty('className')).toBe(false);
}
},
"installationId":"string",
"deviceToken":"string",
"deviceType":"string",
"channels":"array",
"user":"*_User",
expect(schema.hasOwnProperty('fields')).toBe(false);
expect(schema.hasOwnProperty('classLevelPermissions')).toBe(false);
});
}
let schema;
config.database.loadSchema().then(s => {
schema = s;
return schema.getOneSchema('_User', false);
}).then(userSchema => {
validateSchemaStructure(userSchema);
validateSchemaDataStructure(schema.data);
return schema.getOneSchema('_PushStatus', true);
}).then(pushStatusSchema => {
validateSchemaStructure(pushStatusSchema);
validateSchemaDataStructure(schema.data);
done();
});
done();
});
});
describe('Class Level Permissions for requiredAuth', () => {
beforeEach(() => {
config = new Config('test');
});
function createUser() {
const user = new Parse.User();
user.set("username", "hello");
user.set("password", "world");
return user.signUp(null);
}
it('required auth test find', (done) => {
config.database.loadSchema().then((schema) => {
// Just to create a valid class
return schema.validateObject('Stuff', {foo: 'bar'});
}).then((schema) => {
return schema.setPermissions('Stuff', {
'find': {
'requiresAuthentication': true
}
});
}).then(() => {
var query = new Parse.Query('Stuff');
return query.find();
}).then(() => {
fail('Class permissions should have rejected this query.');
done();
}, (e) => {
expect(e.message).toEqual('Permission denied, user needs to be authenticated.');
done();
});
});
it('required auth test find authenticated', (done) => {
config.database.loadSchema().then((schema) => {
// Just to create a valid class
return schema.validateObject('Stuff', {foo: 'bar'});
}).then((schema) => {
return schema.setPermissions('Stuff', {
'find': {
'requiresAuthentication': true
}
});
}).then(() => {
return createUser();
}).then(() => {
var query = new Parse.Query('Stuff');
return query.find();
}).then((results) => {
expect(results.length).toEqual(0);
done();
}, (e) => {
console.error(e);
fail("Should not have failed");
done();
});
});
it('required auth should allow create authenticated', (done) => {
config.database.loadSchema().then((schema) => {
// Just to create a valid class
return schema.validateObject('Stuff', {foo: 'bar'});
}).then((schema) => {
return schema.setPermissions('Stuff', {
'create': {
'requiresAuthentication': true
}
});
}).then(() => {
return createUser();
}).then(() => {
const stuff = new Parse.Object('Stuff');
stuff.set('foo', 'bar');
return stuff.save();
}).then(() => {
done();
}, (e) => {
console.error(e);
fail("Should not have failed");
done();
});
});
it('required auth should reject create when not authenticated', (done) => {
config.database.loadSchema().then((schema) => {
// Just to create a valid class
return schema.validateObject('Stuff', {foo: 'bar'});
}).then((schema) => {
return schema.setPermissions('Stuff', {
'create': {
'requiresAuthentication': true
}
});
}).then(() => {
const stuff = new Parse.Object('Stuff');
stuff.set('foo', 'bar');
return stuff.save();
}).then(() => {
fail('Class permissions should have rejected this query.');
done();
}, (e) => {
expect(e.message).toEqual('Permission denied, user needs to be authenticated.');
done();
});
});
it('required auth test create/get/update/delete authenticated', (done) => {
config.database.loadSchema().then((schema) => {
// Just to create a valid class
return schema.validateObject('Stuff', {foo: 'bar'});
}).then((schema) => {
return schema.setPermissions('Stuff', {
'create': {
'requiresAuthentication': true
},
'get': {
'requiresAuthentication': true
},
'delete': {
'requiresAuthentication': true
},
'update': {
'requiresAuthentication': true
}
});
}).then(() => {
return createUser();
}).then(() => {
const stuff = new Parse.Object('Stuff');
stuff.set('foo', 'bar');
return stuff.save().then(() => {
const query = new Parse.Query('Stuff');
return query.get(stuff.id);
});
}).then((gotStuff) => {
return gotStuff.save({'foo': 'baz'}).then(() => {
return gotStuff.destroy();
})
}).then(() => {
done();
}, (e) => {
console.error(e);
fail("Should not have failed");
done();
});
});
it('required auth test create/get/update/delete not authenitcated', (done) => {
config.database.loadSchema().then((schema) => {
// Just to create a valid class
return schema.validateObject('Stuff', {foo: 'bar'});
}).then((schema) => {
return schema.setPermissions('Stuff', {
'get': {
'requiresAuthentication': true
},
'delete': {
'requiresAuthentication': true
},
'update': {
'requiresAuthentication': true
},
'create': {
'*': true
}
});
}).then(() => {
const stuff = new Parse.Object('Stuff');
stuff.set('foo', 'bar');
return stuff.save().then(() => {
const query = new Parse.Query('Stuff');
return query.get(stuff.id);
});
}).then(() => {
fail("Should not succeed!");
done();
}, (e) => {
expect(e.message).toEqual('Permission denied, user needs to be authenticated.');
done();
});
});
it('required auth test create/get/update/delete not authenitcated', (done) => {
config.database.loadSchema().then((schema) => {
// Just to create a valid class
return schema.validateObject('Stuff', {foo: 'bar'});
}).then((schema) => {
return schema.setPermissions('Stuff', {
'find': {
'requiresAuthentication': true
},
'delete': {
'requiresAuthentication': true
},
'update': {
'requiresAuthentication': true
},
'create': {
'*': true
},
'get': {
'*': true
}
});
}).then(() => {
const stuff = new Parse.Object('Stuff');
stuff.set('foo', 'bar');
return stuff.save().then(() => {
const query = new Parse.Query('Stuff');
return query.get(stuff.id);
})
}).then((result) => {
expect(result.get('foo')).toEqual('bar');
const query = new Parse.Query('Stuff');
return query.find();
}).then(() => {
fail("Should not succeed!");
done();
}, (e) => {
expect(e.message).toEqual('Permission denied, user needs to be authenticated.');
done();
});
});
})
+68
View File
@@ -0,0 +1,68 @@
var CacheController = require('../src/Controllers/CacheController.js').default;
var InMemoryCacheAdapter = require('../src/Adapters/Cache/InMemoryCacheAdapter').default;
var SchemaCache = require('../src/Controllers/SchemaCache').default;
describe('SchemaCache', () => {
let cacheController;
beforeEach(() => {
const cacheAdapter = new InMemoryCacheAdapter({});
cacheController = new CacheController(cacheAdapter, 'appId');
});
it('can retrieve a single schema after all schemas stored', (done) => {
const schemaCache = new SchemaCache(cacheController);
const allSchemas = [{
className: 'Class1'
}, {
className: 'Class2'
}];
schemaCache.setAllClasses(allSchemas).then(() => {
return schemaCache.getOneSchema('Class2');
}).then((schema) => {
expect(schema).not.toBeNull();
done();
});
});
it('does not return all schemas after a single schema is stored', (done) => {
const schemaCache = new SchemaCache(cacheController);
const schema = {
className: 'Class1'
};
schemaCache.setOneSchema(schema.className, schema).then(() => {
return schemaCache.getAllClasses();
}).then((allSchemas) => {
expect(allSchemas).toBeNull();
done();
});
});
it('doesn\'t persist cached data by default', (done) => {
const schemaCache = new SchemaCache(cacheController);
const schema = {
className: 'Class1'
};
schemaCache.setOneSchema(schema.className, schema).then(() => {
const anotherSchemaCache = new SchemaCache(cacheController);
return anotherSchemaCache.getOneSchema(schema.className).then((schema) => {
expect(schema).toBeNull();
done();
});
});
});
it('can persist cached data', (done) => {
const schemaCache = new SchemaCache(cacheController, 5000, true);
const schema = {
className: 'Class1'
};
schemaCache.setOneSchema(schema.className, schema).then(() => {
const anotherSchemaCache = new SchemaCache(cacheController, 5000, true);
return anotherSchemaCache.getOneSchema(schema.className).then((schema) => {
expect(schema).not.toBeNull();
done();
});
});
});
});
+9 -11
View File
@@ -4,20 +4,21 @@ 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);
spyOn(Parse, "Query").and.returnValue({
first: jasmine.createSpy("first").and.returnValue(Parse.Promise.as(new Parse.Object("_Session", {
user: new Parse.User({id:"userId"})
}))),
equalTo: function(){}
})
done();
});
it('can get undefined userId', function(done) {
var sessionTokenCache = new SessionTokenCache();
sessionTokenCache.getUserId(undefined).then((userIdFromCache) => {
sessionTokenCache.getUserId(undefined).then(() => {
}, (error) => {
expect(error).not.toBeNull();
done();
@@ -46,7 +47,4 @@ describe('SessionTokenCache', function() {
});
});
afterEach(function() {
jasmine.restoreLibrary('parse/node', 'User');
});
});
+8 -17
View File
@@ -1,10 +1,10 @@
var Subscription = require('../src/LiveQuery/Subscription').Subscription;
let logger;
describe('Subscription', function() {
beforeEach(function() {
var mockError = jasmine.createSpy('error');
jasmine.mockLibrary('../src/LiveQuery/PLog', 'error', mockError);
logger = require('../src/logger').logger;
spyOn(logger, 'error').and.callThrough();
});
it('can be initialized', function() {
@@ -62,8 +62,7 @@ describe('Subscription', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
subscription.deleteClientSubscription(1, 1);
var PLog =require('../src/LiveQuery/PLog');
expect(PLog.error).toHaveBeenCalled();
expect(logger.error).toHaveBeenCalled();
});
it('can delete nonexistent request for one client', function() {
@@ -71,8 +70,7 @@ describe('Subscription', function() {
subscription.addClientSubscription(1, 1);
subscription.deleteClientSubscription(1, 2);
var PLog =require('../src/LiveQuery/PLog');
expect(PLog.error).toHaveBeenCalled();
expect(logger.error).toHaveBeenCalled();
expect(subscription.clientRequestIds.size).toBe(1);
expect(subscription.clientRequestIds.get(1)).toEqual([1]);
});
@@ -83,8 +81,7 @@ describe('Subscription', function() {
subscription.addClientSubscription(1, 2);
subscription.deleteClientSubscription(1, 2);
var PLog =require('../src/LiveQuery/PLog');
expect(PLog.error).not.toHaveBeenCalled();
expect(logger.error).not.toHaveBeenCalled();
expect(subscription.clientRequestIds.size).toBe(1);
expect(subscription.clientRequestIds.get(1)).toEqual([1]);
});
@@ -96,8 +93,7 @@ describe('Subscription', function() {
subscription.deleteClientSubscription(1, 1);
subscription.deleteClientSubscription(1, 2);
var PLog =require('../src/LiveQuery/PLog');
expect(PLog.error).not.toHaveBeenCalled();
expect(logger.error).not.toHaveBeenCalled();
expect(subscription.clientRequestIds.size).toBe(0);
});
@@ -111,13 +107,8 @@ describe('Subscription', function() {
subscription.deleteClientSubscription(2, 1);
subscription.deleteClientSubscription(2, 2);
var PLog =require('../src/LiveQuery/PLog');
expect(PLog.error).not.toHaveBeenCalled();
expect(logger.error).not.toHaveBeenCalled();
expect(subscription.clientRequestIds.size).toBe(1);
expect(subscription.clientRequestIds.get(1)).toEqual([1]);
});
afterEach(function(){
jasmine.restoreLibrary('../src/LiveQuery/PLog', 'error');
});
});
+50
View File
@@ -0,0 +1,50 @@
const twitter = require('../src/Adapters/Auth/twitter');
describe('Twitter Auth', () => {
it('should use the proper configuration', () => {
// Multiple options, consumer_key found
expect(twitter.handleMultipleConfigurations({
consumer_key: 'hello',
}, [{
consumer_key: 'hello'
}, {
consumer_key: 'world'
}]).consumer_key).toEqual('hello')
// Multiple options, consumer_key not found
expect(function(){
twitter.handleMultipleConfigurations({
consumer_key: 'some',
}, [{
consumer_key: 'hello'
}, {
consumer_key: 'world'
}]);
}).toThrow();
// Multiple options, consumer_key not found
expect(function(){
twitter.handleMultipleConfigurations({
auth_token: 'token',
}, [{
consumer_key: 'hello'
}, {
consumer_key: 'world'
}]);
}).toThrow();
// Single configuration and consumer_key set
expect(twitter.handleMultipleConfigurations({
consumer_key: 'hello',
}, {
consumer_key: 'hello'
}).consumer_key).toEqual('hello');
// General case, only 1 config, no consumer_key set
expect(twitter.handleMultipleConfigurations({
auth_token: 'token',
}, {
consumer_key: 'hello'
}).consumer_key).toEqual('hello');
});
});
+102
View File
@@ -0,0 +1,102 @@
'use strict';
const Parse = require("parse/node");
const Config = require('../src/Config');
describe('Uniqueness', function() {
it('fail when create duplicate value in unique field', done => {
const obj = new Parse.Object('UniqueField');
obj.set('unique', 'value');
obj.save().then(() => {
expect(obj.id).not.toBeUndefined();
const config = new Config('test');
return config.database.adapter.ensureUniqueness('UniqueField', { fields: { unique: { __type: 'String' } } }, ['unique'])
})
.then(() => {
const obj = new Parse.Object('UniqueField');
obj.set('unique', 'value');
return obj.save()
}).then(() => {
fail('Saving duplicate field should have failed');
done();
}, error => {
expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE);
done();
});
});
it('unique indexing works on pointer fields', done => {
const obj = new Parse.Object('UniquePointer');
obj.save({ string: 'who cares' })
.then(() => obj.save({ ptr: obj }))
.then(() => {
const config = new Config('test');
return config.database.adapter.ensureUniqueness('UniquePointer', { fields: {
string: { __type: 'String' },
ptr: { __type: 'Pointer', targetClass: 'UniquePointer' }
} }, ['ptr']);
})
.then(() => {
const newObj = new Parse.Object('UniquePointer')
newObj.set('ptr', obj)
return newObj.save()
})
.then(() => {
fail('save should have failed due to duplicate value');
done();
})
.catch(error => {
expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE);
done();
});
});
it('fails when attempting to ensure uniqueness of fields that are not currently unique', done => {
const o1 = new Parse.Object('UniqueFail');
o1.set('key', 'val');
const o2 = new Parse.Object('UniqueFail');
o2.set('key', 'val');
Parse.Object.saveAll([o1, o2])
.then(() => {
const config = new Config('test');
return config.database.adapter.ensureUniqueness('UniqueFail', { fields: { key: { __type: 'String' } } }, ['key']);
})
.catch(error => {
expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE);
done();
});
});
it_exclude_dbs(['postgres'])('can do compound uniqueness', done => {
const config = new Config('test');
config.database.adapter.ensureUniqueness('CompoundUnique', { fields: { k1: { __type: 'String' }, k2: { __type: 'String' } } }, ['k1', 'k2'])
.then(() => {
const o1 = new Parse.Object('CompoundUnique');
o1.set('k1', 'v1');
o1.set('k2', 'v2');
return o1.save();
})
.then(() => {
const o2 = new Parse.Object('CompoundUnique');
o2.set('k1', 'v1');
o2.set('k2', 'not a dupe');
return o2.save();
})
.then(() => {
const o3 = new Parse.Object('CompoundUnique');
o3.set('k1', 'not a dupe');
o3.set('k2', 'v2');
return o3.save();
})
.then(() => {
const o4 = new Parse.Object('CompoundUnique');
o4.set('k1', 'v1');
o4.set('k2', 'v2');
return o4.save();
})
.catch(error => {
expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE);
done();
});
});
});
+59
View File
@@ -0,0 +1,59 @@
var UserController = require('../src/Controllers/UserController').UserController;
var emailAdapter = require('./MockEmailAdapter')
var AppCache = require('../src/cache').AppCache;
describe('UserController', () => {
var user = {
_email_verify_token: 'testToken',
username: 'testUser',
email: 'test@example.com'
}
describe('sendVerificationEmail', () => {
describe('parseFrameURL not provided', () => {
it('uses publicServerURL', (done) => {
AppCache.put(defaultConfiguration.appId, Object.assign({}, defaultConfiguration, {
publicServerURL: 'http://www.example.com',
customPages: {
parseFrameURL: undefined
}
}))
emailAdapter.sendVerificationEmail = (options) => {
expect(options.link).toEqual('http://www.example.com/apps/test/verify_email?token=testToken&username=testUser')
done()
}
var userController = new UserController(emailAdapter, 'test', {
verifyUserEmails: true
})
userController.sendVerificationEmail(user)
})
})
describe('parseFrameURL provided', () => {
it('uses parseFrameURL and includes the destination in the link parameter', (done) => {
AppCache.put(defaultConfiguration.appId, Object.assign({}, defaultConfiguration, {
publicServerURL: 'http://www.example.com',
customPages: {
parseFrameURL: 'http://someother.example.com/handle-parse-iframe'
}
}))
emailAdapter.sendVerificationEmail = (options) => {
expect(options.link).toEqual('http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=testToken&username=testUser')
done()
}
var userController = new UserController(emailAdapter, 'test', {
verifyUserEmails: true
})
userController.sendVerificationEmail(user)
})
})
})
});
+503
View File
@@ -0,0 +1,503 @@
'use strict';
const Parse = require('parse/node');
const request = require('request-promise');
// const Config = require('../src/Config');
const EMAIL = 'foo@bar.com';
const ZIP = '10001';
const SSN = '999-99-9999';
describe('Personally Identifiable Information', () => {
let user;
beforeEach(done => {
return Parse.User.signUp('tester', 'abc')
.then(loggedInUser => user = loggedInUser)
.then(() => Parse.User.logIn(user.get('username'), 'abc'))
.then(() => user
.set('email', EMAIL)
.set('zip', ZIP)
.set('ssn', SSN)
.save())
.then(() => done());
});
it('should be able to get own PII via API with object', (done) => {
const userObj = new (Parse.Object.extend(Parse.User));
userObj.id = user.id;
userObj.fetch().then(
fetchedUser => {
expect(fetchedUser.get('email')).toBe(EMAIL);
}, e => console.error('error', e))
.done(() => done());
});
it('should not be able to get PII via API with object', (done) => {
Parse.User.logOut()
.then(() => {
const userObj = new (Parse.Object.extend(Parse.User));
userObj.id = user.id;
userObj.fetch().then(
fetchedUser => {
expect(fetchedUser.get('email')).toBe(undefined);
})
.fail(e => {
done.fail(JSON.stringify(e));
})
.done(() => done());
});
});
it('should be able to get PII via API with object using master key', (done) => {
Parse.User.logOut()
.then(() => {
const userObj = new (Parse.Object.extend(Parse.User));
userObj.id = user.id;
userObj.fetch({ useMasterKey: true }).then(
fetchedUser => {
expect(fetchedUser.get('email')).toBe(EMAIL);
}, e => console.error('error', e))
.done(() => done());
});
});
it('should be able to get own PII via API with Find', (done) => {
new Parse.Query(Parse.User)
.first()
.then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(EMAIL);
expect(fetchedUser.get('zip')).toBe(ZIP);
expect(fetchedUser.get('ssn')).toBe(SSN);
done();
});
});
it('should not get PII via API with Find', (done) => {
Parse.User.logOut()
.then(() => new Parse.Query(Parse.User)
.first()
.then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(undefined);
expect(fetchedUser.get('zip')).toBe(ZIP);
expect(fetchedUser.get('ssn')).toBe(SSN);
done();
})
);
});
it('should get PII via API with Find using master key', (done) => {
Parse.User.logOut()
.then(() => new Parse.Query(Parse.User)
.first({ useMasterKey: true })
.then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(EMAIL);
expect(fetchedUser.get('zip')).toBe(ZIP);
expect(fetchedUser.get('ssn')).toBe(SSN);
done();
})
);
});
it('should be able to get own PII via API with Get', (done) => {
new Parse.Query(Parse.User)
.get(user.id)
.then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(EMAIL);
expect(fetchedUser.get('zip')).toBe(ZIP);
expect(fetchedUser.get('ssn')).toBe(SSN);
done();
});
});
it('should not get PII via API with Get', (done) => {
Parse.User.logOut()
.then(() => new Parse.Query(Parse.User)
.get(user.id)
.then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(undefined);
expect(fetchedUser.get('zip')).toBe(ZIP);
expect(fetchedUser.get('ssn')).toBe(SSN);
done();
})
);
});
it('should get PII via API with Get using master key', (done) => {
Parse.User.logOut()
.then(() => new Parse.Query(Parse.User)
.get(user.id, { useMasterKey: true })
.then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(EMAIL);
expect(fetchedUser.get('zip')).toBe(ZIP);
expect(fetchedUser.get('ssn')).toBe(SSN);
done();
})
);
});
it('should not get PII via REST', (done) => {
request.get({
url: 'http://localhost:8378/1/classes/_User',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Javascript-Key': 'test'
}
})
.then(
result => {
const fetchedUser = result.results[0];
expect(fetchedUser.zip).toBe(ZIP);
expect(fetchedUser.email).toBe(undefined);
},
e => console.error('error', e.message)
).done(() => done());
});
it('should get PII via REST with self credentials', (done) => {
request.get({
url: 'http://localhost:8378/1/classes/_User',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Javascript-Key': 'test',
'X-Parse-Session-Token': user.getSessionToken()
}
})
.then(
result => {
const fetchedUser = result.results[0];
expect(fetchedUser.zip).toBe(ZIP);
expect(fetchedUser.email).toBe(EMAIL);
},
e => console.error('error', e.message)
).done(() => done());
});
it('should get PII via REST using master key', (done) => {
request.get({
url: 'http://localhost:8378/1/classes/_User',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test'
}
})
.then(
result => {
const fetchedUser = result.results[0];
expect(fetchedUser.zip).toBe(ZIP);
expect(fetchedUser.email).toBe(EMAIL);
},
e => console.error('error', e.message)
).done(() => done());
});
it('should not get PII via REST by ID', (done) => {
request.get({
url: `http://localhost:8378/1/classes/_User/${user.id}`,
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Javascript-Key': 'test'
}
})
.then(
result => {
const fetchedUser = result;
expect(fetchedUser.zip).toBe(ZIP);
expect(fetchedUser.email).toBe(undefined);
},
e => console.error('error', e.message)
).done(() => done());
});
it('should get PII via REST by ID with self credentials', (done) => {
request.get({
url: `http://localhost:8378/1/classes/_User/${user.id}`,
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Javascript-Key': 'test',
'X-Parse-Session-Token': user.getSessionToken()
}
})
.then(
result => {
const fetchedUser = result;
expect(fetchedUser.zip).toBe(ZIP);
expect(fetchedUser.email).toBe(EMAIL);
},
e => console.error('error', e.message)
).done(() => done());
});
it('should get PII via REST by ID with master key', (done) => {
request.get({
url: `http://localhost:8378/1/classes/_User/${user.id}`,
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Javascript-Key': 'test',
'X-Parse-Master-Key': 'test',
}
})
.then(
result => {
const fetchedUser = result;
expect(fetchedUser.zip).toBe(ZIP);
expect(fetchedUser.email).toBe(EMAIL);
},
e => console.error('error', e.message)
).done(() => done());
});
describe('with configured sensitive fields', () => {
beforeEach((done) => {
reconfigureServer({ userSensitiveFields: ['ssn', 'zip'] })
.then(() => done());
});
it('should be able to get own PII via API with object', (done) => {
const userObj = new (Parse.Object.extend(Parse.User));
userObj.id = user.id;
userObj.fetch().then(
fetchedUser => {
expect(fetchedUser.get('email')).toBe(EMAIL);
expect(fetchedUser.get('zip')).toBe(ZIP);
expect(fetchedUser.get('ssn')).toBe(SSN);
done();
}, e => done.fail(e));
});
it('should not be able to get PII via API with object', (done) => {
Parse.User.logOut()
.then(() => {
const userObj = new (Parse.Object.extend(Parse.User));
userObj.id = user.id;
userObj.fetch().then(
fetchedUser => {
expect(fetchedUser.get('email')).toBe(undefined);
expect(fetchedUser.get('zip')).toBe(undefined);
expect(fetchedUser.get('ssn')).toBe(undefined);
}, e => console.error('error', e))
.done(() => done());
});
});
it('should be able to get PII via API with object using master key', (done) => {
Parse.User.logOut()
.then(() => {
const userObj = new (Parse.Object.extend(Parse.User));
userObj.id = user.id;
userObj.fetch({ useMasterKey: true }).then(
fetchedUser => {
expect(fetchedUser.get('email')).toBe(EMAIL);
expect(fetchedUser.get('zip')).toBe(ZIP);
expect(fetchedUser.get('ssn')).toBe(SSN);
}, e => console.error('error', e))
.done(() => done());
});
});
it('should be able to get own PII via API with Find', (done) => {
new Parse.Query(Parse.User)
.first()
.then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(EMAIL);
expect(fetchedUser.get('zip')).toBe(ZIP);
expect(fetchedUser.get('ssn')).toBe(SSN);
done();
});
});
it('should not get PII via API with Find', (done) => {
Parse.User.logOut()
.then(() => new Parse.Query(Parse.User)
.first()
.then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(undefined);
expect(fetchedUser.get('zip')).toBe(undefined);
expect(fetchedUser.get('ssn')).toBe(undefined);
done();
})
);
});
it('should get PII via API with Find using master key', (done) => {
Parse.User.logOut()
.then(() => new Parse.Query(Parse.User)
.first({ useMasterKey: true })
.then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(EMAIL);
expect(fetchedUser.get('zip')).toBe(ZIP);
expect(fetchedUser.get('ssn')).toBe(SSN);
done();
})
);
});
it('should be able to get own PII via API with Get', (done) => {
new Parse.Query(Parse.User)
.get(user.id)
.then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(EMAIL);
expect(fetchedUser.get('zip')).toBe(ZIP);
expect(fetchedUser.get('ssn')).toBe(SSN);
done();
});
});
it('should not get PII via API with Get', (done) => {
Parse.User.logOut()
.then(() => new Parse.Query(Parse.User)
.get(user.id)
.then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(undefined);
expect(fetchedUser.get('zip')).toBe(undefined);
expect(fetchedUser.get('ssn')).toBe(undefined);
done();
})
);
});
it('should get PII via API with Get using master key', (done) => {
Parse.User.logOut()
.then(() => new Parse.Query(Parse.User)
.get(user.id, { useMasterKey: true })
.then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(EMAIL);
expect(fetchedUser.get('zip')).toBe(ZIP);
expect(fetchedUser.get('ssn')).toBe(SSN);
done();
})
);
});
it('should not get PII via REST', (done) => {
request.get({
url: 'http://localhost:8378/1/classes/_User',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Javascript-Key': 'test'
}
})
.then(
result => {
const fetchedUser = result.results[0];
expect(fetchedUser.zip).toBe(undefined);
expect(fetchedUser.ssn).toBe(undefined);
expect(fetchedUser.email).toBe(undefined);
},
e => console.error('error', e.message)
).done(() => done());
});
it('should get PII via REST with self credentials', (done) => {
request.get({
url: 'http://localhost:8378/1/classes/_User',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Javascript-Key': 'test',
'X-Parse-Session-Token': user.getSessionToken()
}
})
.then(
result => {
const fetchedUser = result.results[0];
expect(fetchedUser.zip).toBe(ZIP);
expect(fetchedUser.email).toBe(EMAIL);
expect(fetchedUser.ssn).toBe(SSN);
},
e => console.error('error', e.message)
).done(() => done());
});
it('should get PII via REST using master key', (done) => {
request.get({
url: 'http://localhost:8378/1/classes/_User',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test'
}
})
.then(
result => {
const fetchedUser = result.results[0];
expect(fetchedUser.zip).toBe(ZIP);
expect(fetchedUser.email).toBe(EMAIL);
expect(fetchedUser.ssn).toBe(SSN);
},
e => console.error('error', e.message)
).done(() => done());
});
it('should not get PII via REST by ID', (done) => {
request.get({
url: `http://localhost:8378/1/classes/_User/${user.id}`,
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Javascript-Key': 'test'
}
})
.then(
result => {
const fetchedUser = result;
expect(fetchedUser.zip).toBe(undefined);
expect(fetchedUser.email).toBe(undefined);
},
e => console.error('error', e.message)
).done(() => done());
});
it('should get PII via REST by ID with self credentials', (done) => {
request.get({
url: `http://localhost:8378/1/classes/_User/${user.id}`,
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Javascript-Key': 'test',
'X-Parse-Session-Token': user.getSessionToken()
}
})
.then(
result => {
const fetchedUser = result;
expect(fetchedUser.zip).toBe(ZIP);
expect(fetchedUser.email).toBe(EMAIL);
},
e => console.error('error', e.message)
).done(() => done());
});
it('should get PII via REST by ID with master key', (done) => {
request.get({
url: `http://localhost:8378/1/classes/_User/${user.id}`,
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Javascript-Key': 'test',
'X-Parse-Master-Key': 'test',
}
})
.then(
result => {
const fetchedUser = result;
expect(fetchedUser.zip).toBe(ZIP);
expect(fetchedUser.email).toBe(EMAIL);
},
e => console.error('error', e.message)
).done(() => done());
});
});
});
File diff suppressed because it is too large Load Diff
+99
View File
@@ -0,0 +1,99 @@
'use strict';
var WinstonLoggerAdapter = require('../src/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter;
var request = require('request');
describe('info logs', () => {
it("Verify INFO logs", (done) => {
var winstonLoggerAdapter = new WinstonLoggerAdapter();
winstonLoggerAdapter.log('info', 'testing info logs', () => {
winstonLoggerAdapter.query({
from: new Date(Date.now() - 500),
size: 100,
level: 'info'
}, (results) => {
if (results.length == 0) {
fail('The adapter should return non-empty results');
} else {
expect(results[0].message).toEqual('testing info logs');
}
// Check the error log
// Regression #2639
winstonLoggerAdapter.query({
from: new Date(Date.now() - 500),
size: 100,
level: 'error'
}, (results) => {
expect(results.length).toEqual(0);
done();
});
});
});
});
});
describe('error logs', () => {
it("Verify ERROR logs", (done) => {
var winstonLoggerAdapter = new WinstonLoggerAdapter();
winstonLoggerAdapter.log('error', 'testing error logs', () => {
winstonLoggerAdapter.query({
from: new Date(Date.now() - 500),
size: 100,
level: 'error'
}, (results) => {
if(results.length == 0) {
fail('The adapter should return non-empty results');
done();
}
else {
expect(results[0].message).toEqual('testing error logs');
done();
}
});
});
});
});
describe('verbose logs', () => {
it("mask sensitive information in _User class", (done) => {
reconfigureServer({ verbose: true })
.then(() => createTestUser())
.then(() => {
const winstonLoggerAdapter = new WinstonLoggerAdapter();
return winstonLoggerAdapter.query({
from: new Date(Date.now() - 500),
size: 100,
level: 'verbose'
});
}).then((results) => {
const logString = JSON.stringify(results);
expect(logString.match(/\*\*\*\*\*\*\*\*/g).length).not.toBe(0);
expect(logString.match(/moon-y/g)).toBe(null);
var headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'
};
request.get({
headers: headers,
url: 'http://localhost:8378/1/login?username=test&password=moon-y'
}, () => {
const winstonLoggerAdapter = new WinstonLoggerAdapter();
return winstonLoggerAdapter.query({
from: new Date(Date.now() - 500),
size: 100,
level: 'verbose'
}).then((results) => {
const logString = JSON.stringify(results);
expect(logString.match(/\*\*\*\*\*\*\*\*/g).length).not.toBe(0);
expect(logString.match(/moon-y/g)).toBe(null);
done();
});
});
}).catch((err) => {
fail(JSON.stringify(err));
done();
})
});
});
+44
View File
@@ -0,0 +1,44 @@
var batch = require('../src/batch');
const originalURL = '/parse/batch';
const serverURL = 'http://localhost:1234/parse';
const serverURL1 = 'http://localhost:1234/1';
const serverURLNaked = 'http://localhost:1234/';
const publicServerURL = 'http://domain.com/parse';
const publicServerURLNaked = 'http://domain.com/';
describe('batch', () => {
it('should return the proper url', () => {
const internalURL = batch.makeBatchRoutingPathFunction(originalURL)('/parse/classes/Object');
expect(internalURL).toEqual('/classes/Object');
});
it('should return the proper url same public/local endpoint', () => {
const originalURL = '/parse/batch';
const internalURL = batch.makeBatchRoutingPathFunction(originalURL, serverURL, publicServerURL)('/parse/classes/Object');
expect(internalURL).toEqual('/classes/Object');
});
it('should return the proper url with different public/local mount', () => {
const originalURL = '/parse/batch';
const internalURL = batch.makeBatchRoutingPathFunction(originalURL, serverURL1, publicServerURL)('/parse/classes/Object');
expect(internalURL).toEqual('/classes/Object');
});
it('should return the proper url with naked public', () => {
const originalURL = '/batch';
const internalURL = batch.makeBatchRoutingPathFunction(originalURL, serverURL, publicServerURLNaked)('/classes/Object');
expect(internalURL).toEqual('/classes/Object');
});
it('should return the proper url with naked local', () => {
const originalURL = '/parse/batch';
const internalURL = batch.makeBatchRoutingPathFunction(originalURL, serverURLNaked, publicServerURL)('/parse/classes/Object');
expect(internalURL).toEqual('/classes/Object');
});
});
+3
View File
@@ -0,0 +1,3 @@
Parse.Cloud.define('cloudCodeInFile', (req, res) => {
res.success('It is possible to define cloud code in a file.');
});

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