mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
213bfea62d | ||
|
|
a17e54c397 | ||
|
|
5773143973 | ||
|
|
406f237119 | ||
|
|
bc384e97e1 | ||
|
|
18906f17b4 | ||
|
|
442c723350 | ||
|
|
9053b941ee | ||
|
|
7e52c18893 | ||
|
|
8c92b80b8f | ||
|
|
27dd2ebf5d | ||
|
|
c1ef0440b0 | ||
|
|
f4889a3393 | ||
|
|
19da14f3df | ||
|
|
91de7504fe | ||
|
|
ec6a39718d | ||
|
|
0e3636d880 | ||
|
|
89df778ac8 | ||
|
|
acc23d0828 | ||
|
|
3a28352c96 | ||
|
|
f99b5588ab | ||
|
|
51664c8f33 | ||
|
|
8c12f80d28 | ||
|
|
c346e15a18 | ||
|
|
4fd67a51fb | ||
|
|
fffc83c0d9 | ||
|
|
04e8e3e233 | ||
|
|
02af61dc9a | ||
|
|
57156cf52b | ||
|
|
01af755d18 | ||
|
|
3f8a1aae02 | ||
|
|
edfa4092c0 | ||
|
|
431d864ac3 | ||
|
|
647987534f | ||
|
|
ca7d8580e3 | ||
|
|
9c528c6fe8 | ||
|
|
2b3bf7c2b7 | ||
|
|
6540ad1483 | ||
|
|
b5625bc2e5 | ||
|
|
09279d1987 | ||
|
|
dbf64a6d83 | ||
|
|
eeb3331167 | ||
|
|
8a6903aa8d | ||
|
|
434ac91129 | ||
|
|
f0ebb7bae6 | ||
|
|
781e81d36b | ||
|
|
73bca3b64c | ||
|
|
6311c95785 | ||
|
|
efa7366250 | ||
|
|
6b6e684d55 | ||
|
|
5d99075663 | ||
|
|
c5636e1b77 | ||
|
|
ab1858616b | ||
|
|
632c8054da | ||
|
|
7afc08abe4 | ||
|
|
33c9730be2 | ||
|
|
97d3deb73b | ||
|
|
a0e7739526 | ||
|
|
7064f1d3ed | ||
|
|
4774e90937 | ||
|
|
6055f2a552 | ||
|
|
b2819df7e5 | ||
|
|
5dc5ef239e | ||
|
|
f25bcc490a | ||
|
|
aa5ebd49f0 | ||
|
|
106ac95622 | ||
|
|
1f4393cd4d | ||
|
|
e361ccf5a5 | ||
|
|
342a3a92fe | ||
|
|
507fa54570 | ||
|
|
1b8e613dfb | ||
|
|
f259a3b17e | ||
|
|
6be067cff5 | ||
|
|
73f8bbc2b3 | ||
|
|
337d3c297a | ||
|
|
cb4b036947 | ||
|
|
ef08dcf76c | ||
|
|
9dd860948a | ||
|
|
6dc7aa87c6 | ||
|
|
3dccd61222 | ||
|
|
592334756b | ||
|
|
247a06f399 | ||
|
|
74b847f1d1 | ||
|
|
75f49b4d1a | ||
|
|
5c1fe3a325 | ||
|
|
cd05e3bd19 | ||
|
|
236c7d15c2 | ||
|
|
6b9fd16273 | ||
|
|
94b10de7b8 | ||
|
|
aeda714b87 | ||
|
|
114f7d1b58 | ||
|
|
d47a756f80 | ||
|
|
6bea950b32 | ||
|
|
1796ae3b7c | ||
|
|
8731337677 | ||
|
|
f2f7c8bad3 | ||
|
|
e3cb473903 | ||
|
|
ebfbe5794c | ||
|
|
7cdf85b367 | ||
|
|
b8b86264f9 | ||
|
|
ec35c39724 | ||
|
|
19e6293638 | ||
|
|
7738d33399 | ||
|
|
4ff52de01c | ||
|
|
27a3198d81 | ||
|
|
b4ee31322e | ||
|
|
3e45e04cf1 | ||
|
|
958d77207a | ||
|
|
259de3f104 | ||
|
|
152c7e88b8 | ||
|
|
f28b719b83 | ||
|
|
2f00a02936 | ||
|
|
f4ad4c1af5 | ||
|
|
83f5207d44 | ||
|
|
82ebba4db8 | ||
|
|
3d3ed53c5f | ||
|
|
1e45ac753a | ||
|
|
603bf97c0b | ||
|
|
120c6feb61 | ||
|
|
b3c5e836d5 | ||
|
|
53e152e975 | ||
|
|
ee8f85baef | ||
|
|
7a73d74869 | ||
|
|
047c8e71ff | ||
|
|
cbd1b878b9 | ||
|
|
24146753b9 | ||
|
|
ea211e658f | ||
|
|
16b6ec4879 | ||
|
|
ddb76a8650 | ||
|
|
f5fdedfbc7 | ||
|
|
3ce634bd82 | ||
|
|
b30805b47b | ||
|
|
0e662190b3 | ||
|
|
7d6f359434 | ||
|
|
0a4cc606c6 | ||
|
|
8124abab34 | ||
|
|
b7a723fa7e | ||
|
|
4e39975239 | ||
|
|
e506784741 | ||
|
|
5d19c4ab45 | ||
|
|
cc6c3f46fe | ||
|
|
ffbca83aa3 | ||
|
|
4bd32c8a4b | ||
|
|
260cc21b67 | ||
|
|
fb57d91b83 | ||
|
|
414af99644 | ||
|
|
134b6de2b1 | ||
|
|
a31baa4ebd | ||
|
|
3603b82eac | ||
|
|
937dde592f | ||
|
|
24f0fffb41 | ||
|
|
e4e7e42c65 | ||
|
|
44add9d6ac | ||
|
|
c39ba3bdc7 | ||
|
|
13a33a9497 | ||
|
|
46c66667c1 | ||
|
|
8b7779a0a9 | ||
|
|
9ce2c85622 | ||
|
|
d5931f70b5 | ||
|
|
555e25bf33 | ||
|
|
cf3606246f | ||
|
|
5decaecd4f | ||
|
|
33984617c5 | ||
|
|
e2bcc87810 | ||
|
|
5ff186f212 | ||
|
|
21fb14d8f4 | ||
|
|
7194fb195c | ||
|
|
ad2a29ed15 | ||
|
|
14d3062602 | ||
|
|
5c8ad833c7 | ||
|
|
9624970cd0 | ||
|
|
3837d33ee4 | ||
|
|
7b3448ee52 | ||
|
|
877d29daf8 | ||
|
|
e270964b4b | ||
|
|
e1c475512c | ||
|
|
dd7fad8e96 | ||
|
|
40812e2f53 | ||
|
|
0f7335b534 | ||
|
|
9ce019d128 | ||
|
|
ba545fbf59 | ||
|
|
7346ff2b38 | ||
|
|
461ddcbd05 | ||
|
|
530fad5a58 | ||
|
|
836582fdb1 | ||
|
|
81f3a79f04 | ||
|
|
a30c81745a | ||
|
|
d6af77b170 | ||
|
|
d45fbd7516 | ||
|
|
86f93e39a7 | ||
|
|
6e4357c6df | ||
|
|
9ea344abb7 | ||
|
|
3451fc3430 | ||
|
|
3ed3982e0e | ||
|
|
e00112ec3c | ||
|
|
3e893738ce | ||
|
|
4df9a7e786 | ||
|
|
ad9495bf14 | ||
|
|
cc4e751682 | ||
|
|
18781f1f82 | ||
|
|
0bd21cb808 | ||
|
|
ba77d755e9 | ||
|
|
120f23c791 | ||
|
|
7c387e1ee9 | ||
|
|
05baf36a46 | ||
|
|
dad50d12f5 | ||
|
|
a392c088d8 | ||
|
|
4d401d9daa | ||
|
|
308fe1498a | ||
|
|
2557ea6300 | ||
|
|
cadd6fe406 | ||
|
|
49531e7efe | ||
|
|
8a779210af | ||
|
|
5c880ca546 | ||
|
|
2e41b30179 | ||
|
|
6c437ffb49 | ||
|
|
a642fdeda9 | ||
|
|
91bcb2430c | ||
|
|
ca0df3a27d | ||
|
|
2130d38d04 | ||
|
|
5ef12b55b4 | ||
|
|
2e618f4258 | ||
|
|
c9a398f406 | ||
|
|
b89619b6ee | ||
|
|
7d787328c5 | ||
|
|
daad05a00f | ||
|
|
2c991a4726 | ||
|
|
6e65a8fc6f | ||
|
|
cd7a4b80bf | ||
|
|
1ed868b99c | ||
|
|
bcffcbade2 | ||
|
|
d42d359437 | ||
|
|
8ea2b615a4 | ||
|
|
1e7e4fe372 | ||
|
|
c935ed8364 | ||
|
|
16e3529c96 | ||
|
|
9c5f14981e | ||
|
|
54d154f7aa | ||
|
|
9aaaf78a36 | ||
|
|
b1d399bf80 | ||
|
|
2114153e08 | ||
|
|
76e6f8c775 | ||
|
|
dbf2afc5ea | ||
|
|
2300b212d5 | ||
|
|
d71a58c217 | ||
|
|
d2cba7502b | ||
|
|
dc0e8ebe9a | ||
|
|
b47c927377 | ||
|
|
f08b0b32bb | ||
|
|
ddd1ae3338 | ||
|
|
78425c8a31 | ||
|
|
c9e5a6a72a | ||
|
|
bec49e7dc2 | ||
|
|
e75d233b7e | ||
|
|
64f9fad285 | ||
|
|
5780c1e425 | ||
|
|
d4fd73100c | ||
|
|
51862c1265 | ||
|
|
d86f0a8c69 | ||
|
|
2730398b92 | ||
|
|
72362bc604 | ||
|
|
d594f93567 | ||
|
|
bb5cd080bc | ||
|
|
21444b07e4 | ||
|
|
e93873f7b1 | ||
|
|
3723b9bb75 | ||
|
|
0f07c5204e | ||
|
|
b9f08d9694 | ||
|
|
7143a12a46 | ||
|
|
2f63c5528d | ||
|
|
bd7d951ad6 | ||
|
|
6acb5cee80 | ||
|
|
982fc72327 | ||
|
|
f40efe3923 | ||
|
|
8300dd8a47 | ||
|
|
0c446b9731 | ||
|
|
440099267d | ||
|
|
241cd8c735 | ||
|
|
49eb9df1ef | ||
|
|
fb5b8fb58f | ||
|
|
a163327ac9 | ||
|
|
8d10447c9f | ||
|
|
0abd5a5931 | ||
|
|
438cf58d4c | ||
|
|
98769a2226 | ||
|
|
80869749aa | ||
|
|
bf96f0d28a | ||
|
|
4259c5d377 | ||
|
|
654a540b6a | ||
|
|
de0f71cc9e | ||
|
|
47061d8e98 | ||
|
|
f2ead46580 | ||
|
|
7909f0efe0 | ||
|
|
477e978e2f | ||
|
|
fa6954169e | ||
|
|
cea4b2bd6a | ||
|
|
963811d022 | ||
|
|
f1f9bde385 | ||
|
|
75ae95815d | ||
|
|
2e8f34918e | ||
|
|
5cdcadea36 | ||
|
|
5b1cf9898d | ||
|
|
2c5144028b | ||
|
|
0f00d659cb | ||
|
|
ce35b81cc6 | ||
|
|
53e7337eee | ||
|
|
91ff816839 | ||
|
|
3a06117fa1 | ||
|
|
b36fd6f245 | ||
|
|
84635352e3 | ||
|
|
e92ee7e1c2 | ||
|
|
157e43f4a4 | ||
|
|
f23aed522a | ||
|
|
375e7016be | ||
|
|
00d0d3ea7c | ||
|
|
3fb6a60977 | ||
|
|
fedeff9a20 | ||
|
|
1450795516 | ||
|
|
7e153d48f4 | ||
|
|
3266d59fcc | ||
|
|
908a4eb643 | ||
|
|
2d4c08c5a3 | ||
|
|
4ee9cb754c | ||
|
|
b3a52fb52c | ||
|
|
91eeca3500 | ||
|
|
1a723c648a | ||
|
|
fcc8032e4c | ||
|
|
4f643d970a | ||
|
|
930e8422b5 | ||
|
|
7cc059973b | ||
|
|
17bc79b372 | ||
|
|
c9f8453171 | ||
|
|
fb9f784a87 | ||
|
|
172da3aaa3 | ||
|
|
e074a922fb | ||
|
|
c6ae64bdbe | ||
|
|
a44b1d9f76 | ||
|
|
069605e9c3 | ||
|
|
2c844a11b9 | ||
|
|
3e6c53c3e4 | ||
|
|
ee38ebc4a3 | ||
|
|
1a17d16dc9 | ||
|
|
ab11ef158f | ||
|
|
438e802a6a | ||
|
|
8814e1f3b5 | ||
|
|
295d361725 | ||
|
|
5b8ad9d4c6 | ||
|
|
1dc346715d | ||
|
|
e3d26e2b4e | ||
|
|
33ac89569f | ||
|
|
9a7264100c | ||
|
|
8363122c65 | ||
|
|
06fb8d451a | ||
|
|
b6009bb393 | ||
|
|
4d7c87b104 | ||
|
|
2d509cdf1c | ||
|
|
ec8529ada7 | ||
|
|
50735c4cbb | ||
|
|
b301ea9a9c | ||
|
|
a44e371f40 | ||
|
|
dbcbf60816 | ||
|
|
0e39b3b0e7 | ||
|
|
3b4faaaf95 | ||
|
|
187f29c2a6 | ||
|
|
e64b6860c1 | ||
|
|
907a05c57a | ||
|
|
f157538bfd | ||
|
|
6973de7910 | ||
|
|
90a1e46905 | ||
|
|
99cb05ea1e | ||
|
|
2afebf955f | ||
|
|
c6406b9e1a | ||
|
|
9bc1a3556e | ||
|
|
edc7720660 | ||
|
|
c4fa3f0ee0 | ||
|
|
befcd453a4 | ||
|
|
df4022060e | ||
|
|
1053adf01d | ||
|
|
2cc9600604 | ||
|
|
cd4c4169d5 | ||
|
|
dac71019b6 | ||
|
|
358a7ae7f3 | ||
|
|
c4aac335e0 | ||
|
|
f3b7138269 | ||
|
|
b778b314fb | ||
|
|
d8d374379d | ||
|
|
ee3b37d4a2 | ||
|
|
d872f52eff | ||
|
|
747482227d | ||
|
|
e477fadec4 | ||
|
|
5f9a8d87e5 | ||
|
|
d70e87f141 | ||
|
|
6ddc77601c | ||
|
|
ccc2a1a03f | ||
|
|
a56fe0f7e6 | ||
|
|
36202badf3 | ||
|
|
81519852d1 | ||
|
|
5219e0b1d8 | ||
|
|
1bfee45f64 | ||
|
|
8eff44410f | ||
|
|
4b7447e228 | ||
|
|
818fe492d7 | ||
|
|
bb80e5d69f | ||
|
|
bfafcd4e87 | ||
|
|
43f014a47d | ||
|
|
3629c40036 | ||
|
|
27815b18aa | ||
|
|
c7503fc654 | ||
|
|
4e312673c7 | ||
|
|
63dc64004d | ||
|
|
8e034dd9b0 | ||
|
|
9cc7bd064c | ||
|
|
789dc48f47 | ||
|
|
36aa5935b1 | ||
|
|
4049ce4102 | ||
|
|
6d7813be4a | ||
|
|
244009923f | ||
|
|
e39286d88b | ||
|
|
a909ab71e3 | ||
|
|
4bd163b790 | ||
|
|
4fe670efaf | ||
|
|
63f7468ce7 | ||
|
|
9538a7dab5 | ||
|
|
abde9484ce | ||
|
|
49994b6e87 | ||
|
|
17235b576b | ||
|
|
806800c6fb | ||
|
|
dacc22de42 | ||
|
|
5f9b5d54f3 | ||
|
|
a0150a743f | ||
|
|
4054c247ec | ||
|
|
92e51ab4d3 | ||
|
|
fb4a2524b1 | ||
|
|
e58c935f22 | ||
|
|
8ce0bd84fb | ||
|
|
a79fb3a64c | ||
|
|
72e6eae727 | ||
|
|
750a5794d3 | ||
|
|
9601ca4fca | ||
|
|
ad2e3c9b09 | ||
|
|
66eaf6c6ef | ||
|
|
0e58c8d867 | ||
|
|
7257ee858b | ||
|
|
78d380df72 | ||
|
|
9287afc3c2 | ||
|
|
3889223b10 | ||
|
|
0b990b671a | ||
|
|
382149b190 | ||
|
|
63a534f31d | ||
|
|
d1dbc1a035 | ||
|
|
2733c0924b | ||
|
|
6893895aea | ||
|
|
028ef2a7b2 | ||
|
|
d7b1184b5e | ||
|
|
28d1a8afe4 | ||
|
|
6aa38ea8ca | ||
|
|
2183b0be82 | ||
|
|
3ecaa0aa4b | ||
|
|
91d9724182 | ||
|
|
f3bb2c99e0 | ||
|
|
7dd765256c | ||
|
|
0b307bc22f | ||
|
|
8dc37b9d30 | ||
|
|
d9f1e00345 | ||
|
|
04491fcb20 | ||
|
|
46da50f2b3 | ||
|
|
e9699b64a5 | ||
|
|
eb892830e6 | ||
|
|
7215300c1e | ||
|
|
997da898eb | ||
|
|
d78c2746e9 | ||
|
|
10efd5d38d | ||
|
|
888354154d | ||
|
|
e43c471a7e | ||
|
|
430dd7944e | ||
|
|
3e41a21e27 | ||
|
|
f60c6af5e7 | ||
|
|
88b14a914a | ||
|
|
ae9f196ec7 | ||
|
|
dec95ca4fa | ||
|
|
d067c0ba61 | ||
|
|
360cc3461d | ||
|
|
deafb680ae | ||
|
|
2520e7b6c6 | ||
|
|
bd89338b39 | ||
|
|
97d6e9d1a8 | ||
|
|
4eb9bd442f | ||
|
|
6cafd46d06 | ||
|
|
310650830e | ||
|
|
c359d0fb5f | ||
|
|
768a781f98 | ||
|
|
d30c3e90df | ||
|
|
daa5f11122 | ||
|
|
49a4b74f7c | ||
|
|
9bc636dc5c | ||
|
|
db9b72b147 | ||
|
|
0cf3f56b00 | ||
|
|
02b56def6d | ||
|
|
aee968a853 | ||
|
|
c9d4f7693d | ||
|
|
49b01c961b | ||
|
|
ca9b750993 | ||
|
|
9142dec5fd | ||
|
|
cdb82c20bb | ||
|
|
dd771d429e | ||
|
|
753bead4ac | ||
|
|
34a621b279 | ||
|
|
4e5cc1feb0 | ||
|
|
ea2de87b1a | ||
|
|
97489106b4 | ||
|
|
57c936485e | ||
|
|
6a17eca69c | ||
|
|
1b83d9c621 | ||
|
|
791c5512aa | ||
|
|
7ca8836768 | ||
|
|
3b1165b686 | ||
|
|
54e46d4157 | ||
|
|
5e4cecd94e | ||
|
|
a96cb6b31d | ||
|
|
1945313794 | ||
|
|
839f6b795c | ||
|
|
fa729c5c18 | ||
|
|
98c65062e5 | ||
|
|
e5b1d50343 | ||
|
|
3c6141576f | ||
|
|
c4abd1e267 | ||
|
|
c0714b9b9d | ||
|
|
9ac7a52e40 | ||
|
|
a7262dafd8 | ||
|
|
e58aa54e31 | ||
|
|
c867e37559 | ||
|
|
535853311f | ||
|
|
db5338abaa | ||
|
|
fe11fb54fc | ||
|
|
f58befd7bb | ||
|
|
3ca1e9f79e | ||
|
|
81f5c682d4 | ||
|
|
30aafd2c62 | ||
|
|
ab425d85b0 | ||
|
|
7bbe1665f8 | ||
|
|
970682a499 | ||
|
|
9d342a8aee | ||
|
|
f3286f9d16 | ||
|
|
f2f42fe66d | ||
|
|
f461f2abb3 | ||
|
|
e15f9dc8b1 | ||
|
|
bc90effd32 | ||
|
|
5fae41183e | ||
|
|
a41ca281f0 | ||
|
|
ae82f33118 | ||
|
|
74289f9ce5 | ||
|
|
39dce71e4f | ||
|
|
7a14695da8 | ||
|
|
b477d16992 | ||
|
|
1ae61c92c8 | ||
|
|
63a7e854fd | ||
|
|
24a25d14b3 | ||
|
|
69efeac1a3 | ||
|
|
4643be7086 | ||
|
|
2dc080fd00 | ||
|
|
14cc82a057 | ||
|
|
531e2ffea8 | ||
|
|
b1c2d9da6a | ||
|
|
61e8f2aeea | ||
|
|
b6223f598c | ||
|
|
1f707a97d3 | ||
|
|
18d7a7a2af | ||
|
|
be693fb736 | ||
|
|
17e5686efd | ||
|
|
5755126525 | ||
|
|
1f40c0a7cd | ||
|
|
017863fe7a | ||
|
|
48dcfe37e7 | ||
|
|
8ce8e2bbe0 | ||
|
|
24824757a7 | ||
|
|
3f22d060eb | ||
|
|
57bfbfc76d | ||
|
|
966d10ca43 | ||
|
|
193f368686 | ||
|
|
7da7c43fce | ||
|
|
303388d585 | ||
|
|
0fe2c64288 | ||
|
|
cc3d93f5d9 | ||
|
|
bbc3d36d51 | ||
|
|
d7550710a5 | ||
|
|
fcf745ad1d | ||
|
|
dddb6795cf | ||
|
|
045caca946 | ||
|
|
831c2ee3de | ||
|
|
170dd40d3c | ||
|
|
23e55e941e | ||
|
|
0dda59ff73 | ||
|
|
33fa5a7b2a | ||
|
|
d504681589 | ||
|
|
b774c568e0 | ||
|
|
4097f47b1d | ||
|
|
bd548786ea | ||
|
|
04795ff165 | ||
|
|
fb1c6bb3fb | ||
|
|
9d9052f2e5 | ||
|
|
64d92daefa | ||
|
|
6f4a7a6ff2 | ||
|
|
d9b94c78ef | ||
|
|
fbb5e448e6 | ||
|
|
305879a251 | ||
|
|
b490688652 | ||
|
|
fbdd89c2d1 | ||
|
|
eb0340585f | ||
|
|
3c4d5159b4 | ||
|
|
067946c66c | ||
|
|
8aaf8f2ff2 | ||
|
|
5378de7398 | ||
|
|
78c5292bc3 | ||
|
|
941984f2e0 | ||
|
|
01f4bcc3e3 | ||
|
|
f8b1d9f303 | ||
|
|
843f289163 | ||
|
|
8441dd2956 | ||
|
|
7db69ac8f2 | ||
|
|
3087b2121e | ||
|
|
2afb28564e | ||
|
|
39f8143326 | ||
|
|
6e55e59b86 | ||
|
|
9c477907bf | ||
|
|
5ed037f8a5 | ||
|
|
3f6445d035 | ||
|
|
02566f0906 | ||
|
|
c9dfac277f | ||
|
|
4bdff73933 | ||
|
|
605f83c36a | ||
|
|
5f757b0741 | ||
|
|
9678b7afee | ||
|
|
a6dc76d52f | ||
|
|
5b8aa22730 | ||
|
|
c9431a8ff3 | ||
|
|
4e453925f7 | ||
|
|
ba68acfb0d | ||
|
|
2e6173fb99 | ||
|
|
d1476ecabd | ||
|
|
5de33ac226 | ||
|
|
d087ae9b35 | ||
|
|
19d17d8610 | ||
|
|
d42b08055b | ||
|
|
b1ab388c96 | ||
|
|
cc55bfb7ba | ||
|
|
67c7670437 | ||
|
|
f271ac9ac1 | ||
|
|
61b4468dac | ||
|
|
d668bd984c | ||
|
|
7cc4ef95c0 | ||
|
|
57e94dc82c | ||
|
|
a254759da8 | ||
|
|
b380cd2bd6 | ||
|
|
86a5ae40e3 | ||
|
|
ce1de0a5ef | ||
|
|
bd0d21af29 | ||
|
|
4ecf0ac10c | ||
|
|
176f1b9085 | ||
|
|
06b8157eea | ||
|
|
5a167a9a46 | ||
|
|
5b40a589c0 | ||
|
|
2c5b77f284 | ||
|
|
173c5d46e3 | ||
|
|
2234a104d4 | ||
|
|
83c0f92553 | ||
|
|
0755a2953e | ||
|
|
220a13392d | ||
|
|
d7d87a31f0 | ||
|
|
ec1bbc8d74 | ||
|
|
42aacdf62b | ||
|
|
2ccd039497 | ||
|
|
5420c38adf | ||
|
|
4822d2b668 | ||
|
|
6adffb09d0 | ||
|
|
035fcd2615 |
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"presets": [
|
||||
"es2015"
|
||||
]
|
||||
"plugins": [
|
||||
"transform-flow-strip-types"
|
||||
],
|
||||
"presets": [
|
||||
"es2015",
|
||||
"stage-0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[ignore]
|
||||
.*/node_modules/
|
||||
.*/lib/
|
||||
|
||||
[include]
|
||||
|
||||
[libs]
|
||||
|
||||
[options]
|
||||
@@ -0,0 +1,19 @@
|
||||
Check out [this issue](https://github.com/ParsePlatform/parse-server/issues/1271) for an ideal bug report. The closer your issue report is to that one, the more likely we are to be able to help, and the more likely we will be to fix the issue quickly!
|
||||
|
||||
For implementation related questions or technical support, please refer to the [Stack Overflow](http://stackoverflow.com/questions/tagged/parse.com) and [Server Fault](https://serverfault.com/tags/parse) communities.
|
||||
|
||||
Make sure these boxes are checked before submitting your issue -- thanks for reporting issues back to Parse Server!
|
||||
|
||||
- [ ] You've met the [prerequisites](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites).
|
||||
|
||||
- [ ] You're running the [latest version](https://github.com/ParsePlatform/parse-server/releases) of Parse Server.
|
||||
|
||||
- [ ] You've searched through [existing issues](https://github.com/ParsePlatform/parse-server/issues?utf8=%E2%9C%93&q=). Chances are that your issue has been reported or resolved before.
|
||||
|
||||
#### Environment Setup
|
||||
|
||||
|
||||
#### Steps to reproduce
|
||||
|
||||
|
||||
#### Logs/Trace
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
+13
@@ -1,5 +1,6 @@
|
||||
# Logs
|
||||
logs
|
||||
test_logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
@@ -32,5 +33,17 @@ node_modules
|
||||
# WebStorm/IntelliJ
|
||||
.idea
|
||||
|
||||
# visual studio code
|
||||
.vscode
|
||||
|
||||
# Babel.js
|
||||
lib/
|
||||
|
||||
# cache folder
|
||||
.cache
|
||||
|
||||
# Mac DS_Store files
|
||||
.DS_Store
|
||||
|
||||
# Folder created by FileSystemAdapter
|
||||
/files
|
||||
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directory
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||
node_modules
|
||||
|
||||
# Emacs
|
||||
*~
|
||||
|
||||
# WebStorm/IntelliJ
|
||||
.idea
|
||||
+15
-6
@@ -1,13 +1,22 @@
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
language: node_js
|
||||
node_js:
|
||||
- "4.3"
|
||||
- '4.3'
|
||||
env:
|
||||
global:
|
||||
- COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**'
|
||||
matrix:
|
||||
- MONGODB_VERSION=2.6.11
|
||||
- MONGODB_VERSION=3.0.8
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.mongodb/versions/downloads
|
||||
after_success: ./node_modules/.bin/codecov
|
||||
- "$HOME/.mongodb/versions/downloads"
|
||||
after_success: "./node_modules/.bin/codecov"
|
||||
deploy:
|
||||
provider: npm
|
||||
email:
|
||||
secure: T3LwSJFcu632SDfCoavcKL39NktWLEOOFzonAjeHAglmMyDp9hvU8xHwQ4JJy2CRA03c+ezAe2gH3Va+jfxvh1JtFUan+p88vaCHHKuNwPSp4WZBPt1ZTDmG+ACG6j+ZWHK+eP7hLvtlCi/M9/v4/bVojpm7v01LLiM+MRhnE9E7eLemQC4zb6RVtf2oGQ3mX51kMeya218kEm5bsQPpRJElI9jwletFa8qYK5AFgaCHpll059RRHQTTE9MTNcN58P/Kf6Qb3KGpKNoaHTXoOD4U6LcXLWvdHONVB6AzGefxg2b/dvtaO4nd5lDooMBR3u9kWpopXkyAuf+fO/8je9JTxO6CDUtDjHRcR2mCEXWX6rculIAbNXOv1ccRYCTQ8aFtYnFlMSb1+QIAdyT6HHmeT5ktk3+XJRuEv0PJJvqHTo+j7tPngjiv5sPNutgGFlOFO90omTbzEkIT/D/LsgXbWm2QKYWejbLiuSINg+FiFiZN90doCf4aCpm2y1Q/9GzvK+eXcpqzPCGxOykE7EuPZK3+S/ZU2oaWL2uSpbKPtC0qxebrneR307aVEa7C85HCkTMMBzE4tjUr8h5HhLqtWvqmDcnBo3PfQhf9cEO6jQLd3qhEVJmcwKz/yOCfgPXlCbRqiKk7j94perpNCIalXtMI9ySYjJNjHsB4600=
|
||||
api_key:
|
||||
secure: WLjhHVAdUkDy6UdNWveTpZqphw9olN0alCpyPpU0cJjlN/hk7YxPP+YHOSVnMZaRZoHM0LL11jPKLf970nymBfvJHDWxKk71c/5xyTX8pBtSxmGmGy23a5g2VrHVMd2JxsI7NEAH500tlFz+01t4E3Steo0NpAkHR3Q51kj01gQy8IumsksfhRc9uvjZ7zjm99Yk4L+cxyei79v4esfpx7Bgm0HTfAAAG/19F+r0hvvFiV517SREDU/YtcX/rIjVepEy1uiLKEohnmYtVIRSA3Hjh6VlHVhdj2WDd/dYrxi/Ioysq2zOM9pZVLamO9asB1e6JrTEipWE9jSZXxsbnfFzuNLxcOjKv1wd3OzQKF/7pGKGiWRTv5Xm19D/FrXoE/ULs6bHcC/Ke8Gs2RxPlOCdvAFehCkyf5P4HOPCQdR7o8Yiuvt+5JWKBflElNbSd4nWgwUOo3Yv8vC4Vj53fwmM+Uqfu3IgYFWktYxCm4RWIKMfB/gtmjcF6QYFfjvEMwAvRfBV81kuynCsnubXWzQeBE/b3JOhBfpGciKCjKfy+tS6bZfFjCtQV98hMMiCPre8Y7PahHDYc65wU9Ake+ZE+dDaSbeV3DZ5JeifLJHzKW2J2dWeRANkOaiSwO9VBC9/rht5ulK5qQ1pB2+sKDToAaiKy6RMlB/HgIoYqsU=
|
||||
on:
|
||||
tags: true
|
||||
repo: ParsePlatform/parse-server
|
||||
|
||||
+212
-2
@@ -1,5 +1,215 @@
|
||||
## Parse Server Changelog
|
||||
|
||||
### 2.2.6 (4/5/2016)
|
||||
|
||||
* Important Fix: Disables find on installation from clients [\#1374](https://github.com/ParsePlatform/parse-server/pull/1374) ([flovilmart](https://github.com/flovilmart))
|
||||
* Adds missing options to the CLI [\#1368](https://github.com/ParsePlatform/parse-server/pull/1368) ([flovilmart](https://github.com/flovilmart))
|
||||
* Removes only master on travis [\#1367](https://github.com/ParsePlatform/parse-server/pull/1367) ([flovilmart](https://github.com/flovilmart))
|
||||
* Auth.\_loadRoles should not query the same role twice. [\#1366](https://github.com/ParsePlatform/parse-server/pull/1366) ([blacha](https://github.com/blacha))
|
||||
|
||||
### 2.2.5 (4/4/2016)
|
||||
|
||||
* Improves config loading and tests [\#1363](https://github.com/ParsePlatform/parse-server/pull/1363) ([flovilmart](https://github.com/flovilmart))
|
||||
* Adds travis configuration to deploy NPM on new version tags [\#1361](https://github.com/ParsePlatform/parse-server/pull/1361) ([gfosco](https://github.com/gfosco))
|
||||
* Inject the default schemas properties when loading it [\#1357](https://github.com/ParsePlatform/parse-server/pull/1357) ([flovilmart](https://github.com/flovilmart))
|
||||
* Adds console transport when testing with VERBOSE=1 [\#1351](https://github.com/ParsePlatform/parse-server/pull/1351) ([flovilmart](https://github.com/flovilmart))
|
||||
* Make notEqual work on relations [\#1350](https://github.com/ParsePlatform/parse-server/pull/1350) ([flovilmart](https://github.com/flovilmart))
|
||||
* Accept only bool for $exists in LiveQuery [\#1315](https://github.com/ParsePlatform/parse-server/pull/1315) ([drew-gross](https://github.com/drew-gross))
|
||||
* Adds more options when using CLI/config [\#1305](https://github.com/ParsePlatform/parse-server/pull/1305) ([flovilmart](https://github.com/flovilmart))
|
||||
* Update error message [\#1297](https://github.com/ParsePlatform/parse-server/pull/1297) ([drew-gross](https://github.com/drew-gross))
|
||||
* Properly let masterKey add fields [\#1291](https://github.com/ParsePlatform/parse-server/pull/1291) ([flovilmart](https://github.com/flovilmart))
|
||||
* Point to \#1271 as how to write a good issue report [\#1290](https://github.com/ParsePlatform/parse-server/pull/1290) ([drew-gross](https://github.com/drew-gross))
|
||||
* Adds ability to override mount with publicServerURL for production uses [\#1287](https://github.com/ParsePlatform/parse-server/pull/1287) ([flovilmart](https://github.com/flovilmart))
|
||||
* Single object queries to use include and keys [\#1280](https://github.com/ParsePlatform/parse-server/pull/1280) ([jeremyjackson89](https://github.com/jeremyjackson89))
|
||||
* Improves report for Push error in logs and \_PushStatus [\#1269](https://github.com/ParsePlatform/parse-server/pull/1269) ([flovilmart](https://github.com/flovilmart))
|
||||
* Removes all stdout/err logs while testing [\#1268](https://github.com/ParsePlatform/parse-server/pull/1268) ([flovilmart](https://github.com/flovilmart))
|
||||
* Matching queries with doesNotExist constraint [\#1250](https://github.com/ParsePlatform/parse-server/pull/1250) ([andrecardoso](https://github.com/andrecardoso))
|
||||
* Added session length option for session tokens to server configuration [\#997](https://github.com/ParsePlatform/parse-server/pull/997) ([Kenishi](https://github.com/Kenishi))
|
||||
* Regression test for \#1259 [\#1286](https://github.com/ParsePlatform/parse-server/pull/1286) ([drew-gross](https://github.com/drew-gross))
|
||||
* Regression test for \#871 [\#1283](https://github.com/ParsePlatform/parse-server/pull/1283) ([drew-gross](https://github.com/drew-gross))
|
||||
* Add a test to repro \#701 [\#1281](https://github.com/ParsePlatform/parse-server/pull/1281) ([drew-gross](https://github.com/drew-gross))
|
||||
* Fix for \#1334: using relative cloud code files broken [\#1353](https://github.com/ParsePlatform/parse-server/pull/1353) ([airdrummingfool](https://github.com/airdrummingfool))
|
||||
* Fix Issue/1288 [\#1346](https://github.com/ParsePlatform/parse-server/pull/1346) ([flovilmart](https://github.com/flovilmart))
|
||||
* Fixes \#1271 [\#1295](https://github.com/ParsePlatform/parse-server/pull/1295) ([drew-gross](https://github.com/drew-gross))
|
||||
* Fixes issue \#1302 [\#1314](https://github.com/ParsePlatform/parse-server/pull/1314) ([flovilmart](https://github.com/flovilmart))
|
||||
* Fixes bug related to include in queries [\#1312](https://github.com/ParsePlatform/parse-server/pull/1312) ([flovilmart](https://github.com/flovilmart))
|
||||
|
||||
|
||||
### 2.2.4 (3/29/2016)
|
||||
|
||||
* Hotfix: fixed imports issue for S3Adapter, GCSAdapter, FileSystemAdapter [\#1263](https://github.com/ParsePlatform/parse-server/pull/1263) ([drew-gross](https://github.com/drew-gross)
|
||||
* Fix: Clean null authData values on _User update [\#1199](https://github.com/ParsePlatform/parse-server/pull/1199) ([yuzeh](https://github.com/yuzeh))
|
||||
|
||||
### 2.2.3 (3/29/2016)
|
||||
|
||||
* Fixed bug with invalid email verification link on email update. [\#1253](https://github.com/ParsePlatform/parse-server/pull/1253) ([kzielonka](https://github.com/kzielonka))
|
||||
* Badge update supports increment as well as Increment [\#1248](https://github.com/ParsePlatform/parse-server/pull/1248) ([flovilmart](https://github.com/flovilmart))
|
||||
* Config/Push Tested with the dashboard. [\#1235](https://github.com/ParsePlatform/parse-server/pull/1235) ([drew-gross](https://github.com/drew-gross))
|
||||
* Better logging with winston [\#1234](https://github.com/ParsePlatform/parse-server/pull/1234) ([flovilmart](https://github.com/flovilmart))
|
||||
* Make GlobalConfig work like parse.com [\#1210](https://github.com/ParsePlatform/parse-server/pull/1210) ([framp](https://github.com/framp))
|
||||
* Improve flattening of results from pushAdapter [\#1204](https://github.com/ParsePlatform/parse-server/pull/1204) ([flovilmart](https://github.com/flovilmart))
|
||||
* Push adapters are provided by external packages [\#1195](https://github.com/ParsePlatform/parse-server/pull/1195) ([flovilmart](https://github.com/flovilmart))
|
||||
* Fix flaky test [\#1188](https://github.com/ParsePlatform/parse-server/pull/1188) ([drew-gross](https://github.com/drew-gross))
|
||||
* Fixes problem affecting finding array pointers [\#1185](https://github.com/ParsePlatform/parse-server/pull/1185) ([flovilmart](https://github.com/flovilmart))
|
||||
* Moves Files adapters to external packages [\#1172](https://github.com/ParsePlatform/parse-server/pull/1172) ([flovilmart](https://github.com/flovilmart))
|
||||
* Mark push as enabled in serverInfo endpoint [\#1164](https://github.com/ParsePlatform/parse-server/pull/1164) ([drew-gross](https://github.com/drew-gross))
|
||||
* Document email adapter [\#1144](https://github.com/ParsePlatform/parse-server/pull/1144) ([drew-gross](https://github.com/drew-gross))
|
||||
* Reset password fix [\#1133](https://github.com/ParsePlatform/parse-server/pull/1133) ([carmenlau](https://github.com/carmenlau))
|
||||
|
||||
### 2.2.2 (3/23/2016)
|
||||
|
||||
* Important Fix: Mounts createLiveQueryServer, fix babel induced problem [\#1153](https://github.com/ParsePlatform/parse-server/pull/1153) (flovilmart)
|
||||
* Move ParseServer to it's own file [\#1166](https://github.com/ParsePlatform/parse-server/pull/1166) (flovilmart)
|
||||
* Update README.md - remove deploy buttons - replace with community links [\#1139](https://github.com/ParsePlatform/parse-server/pull/1139) (drew-gross)
|
||||
* Adds bootstrap.sh [\#1138](https://github.com/ParsePlatform/parse-server/pull/1138) (flovilmart)
|
||||
* Fix: Do not override username [\#1142](https://github.com/ParsePlatform/parse-server/pull/1142) (flovilmart)
|
||||
* Fix: Add pushId back to GCM payload [\#1168](https://github.com/ParsePlatform/parse-server/pull/1168) (wangmengyan95)
|
||||
|
||||
### 2.2.1 (3/22/2016)
|
||||
|
||||
* New: Add FileSystemAdapter file adapter [\#1098](https://github.com/ParsePlatform/parse-server/pull/1098) (dtsolis)
|
||||
* New: Enabled CLP editing [\#1128](https://github.com/ParsePlatform/parse-server/pull/1128) (drew-gross)
|
||||
* Improvement: Reduces the number of connections to mongo created [\#1111](https://github.com/ParsePlatform/parse-server/pull/1111) (flovilmart)
|
||||
* Improvement: Make ParseServer a class [\#980](https://github.com/ParsePlatform/parse-server/pull/980) (flovilmart)
|
||||
* Fix: Adds support for plain object in $add, $addUnique, $remove [\#1114](https://github.com/ParsePlatform/parse-server/pull/1114) (flovilmart)
|
||||
* Fix: Generates default CLP, freezes objects [\#1132](https://github.com/ParsePlatform/parse-server/pull/1132) (flovilmart)
|
||||
* Fix: Properly sets installationId on creating session with 3rd party auth [\#1110](https://github.com/ParsePlatform/parse-server/pull/1110) (flovilmart)
|
||||
|
||||
### 2.2.0 (3/18/2016)
|
||||
|
||||
* New Feature: Real-time functionality with Live Queries! [\#1092](https://github.com/ParsePlatform/parse-server/pull/1092) (wangmengyan95)
|
||||
* Improvement: Push Status API [\#1004](https://github.com/ParsePlatform/parse-server/pull/1004) (flovilmart)
|
||||
* Improvement: Allow client operations on Roles [\#1068](https://github.com/ParsePlatform/parse-server/pull/1068) (flovilmart)
|
||||
* Improvement: Add URI encoding to mongo auth parameters [\#986](https://github.com/ParsePlatform/parse-server/pull/986) (bgw)
|
||||
* Improvement: Adds support for apps key in config file, but only support single app for now [\#979](https://github.com/ParsePlatform/parse-server/pull/979) (flovilmart)
|
||||
* Documentation: Getting Started and Configuring Parse Server [\#988](https://github.com/ParsePlatform/parse-server/pull/988) (hramos)
|
||||
* Fix: Various edge cases with REST API [\#1066](https://github.com/ParsePlatform/parse-server/pull/1066) (flovilmart)
|
||||
* Fix: Makes sure the location in results has the proper objectId [\#1065](https://github.com/ParsePlatform/parse-server/pull/1065) (flovilmart)
|
||||
* Fix: Third-party auth is properly removed when unlinked [\#1081](https://github.com/ParsePlatform/parse-server/pull/1081) (flovilmart)
|
||||
* Fix: Clear the session-user cache when changing \_User objects [\#1072](https://github.com/ParsePlatform/parse-server/pull/1072) (gfosco)
|
||||
* Fix: Bug related to subqueries on unfetched objects [\#1046](https://github.com/ParsePlatform/parse-server/pull/1046) (flovilmart)
|
||||
* Fix: Properly urlencode parameters for email validation and password reset [\#1001](https://github.com/ParsePlatform/parse-server/pull/1001) (flovilmart)
|
||||
* Fix: Better sanitization/decoding of object data for afterSave triggers [\#992](https://github.com/ParsePlatform/parse-server/pull/992) (flovilmart)
|
||||
* Fix: Changes default encoding for httpRequest [\#892](https://github.com/ParsePlatform/parse-server/pull/892) (flovilmart)
|
||||
|
||||
### 2.1.6 (3/11/2016)
|
||||
|
||||
* Improvement: Full query support for badge Increment \(\#931\) [\#983](https://github.com/ParsePlatform/parse-server/pull/983) (flovilmart)
|
||||
* Improvement: Shutdown standalone parse server gracefully [\#958](https://github.com/ParsePlatform/parse-server/pull/958) (raulr)
|
||||
* Improvement: Add database options to ParseServer constructor and pass to MongoStorageAdapter [\#956](https://github.com/ParsePlatform/parse-server/pull/956) (steven-supersolid)
|
||||
* Improvement: AuthData logic refactor [\#952](https://github.com/ParsePlatform/parse-server/pull/952) (flovilmart)
|
||||
* Improvement: Changed FileLoggerAdapterSpec to fail gracefully on Windows [\#946](https://github.com/ParsePlatform/parse-server/pull/946) (aneeshd16)
|
||||
* Improvement: Add new schema collection type and replace all usages of direct mongo collection for schema operations. [\#943](https://github.com/ParsePlatform/parse-server/pull/943) (nlutsenko)
|
||||
* Improvement: Adds CLP API to Schema router [\#898](https://github.com/ParsePlatform/parse-server/pull/898) (flovilmart)
|
||||
* Fix: Cleans up authData null keys on login for android crash [\#978](https://github.com/ParsePlatform/parse-server/pull/978) (flovilmart)
|
||||
* Fix: Do master query for before/afterSaveHook [\#959](https://github.com/ParsePlatform/parse-server/pull/959) (wangmengyan95)
|
||||
* Fix: re-add shebang [\#944](https://github.com/ParsePlatform/parse-server/pull/944) (flovilmart)
|
||||
* Fix: Added test command for Windows support [\#886](https://github.com/ParsePlatform/parse-server/pull/886) (aneeshd16)
|
||||
|
||||
### 2.1.5 (3/9/2016)
|
||||
|
||||
* New: FileAdapter for Google Cloud Storage [\#708](https://github.com/ParsePlatform/parse-server/pull/708) (mcdonamp)
|
||||
* Improvement: Minimize extra schema queries in some scenarios. [\#919](https://github.com/ParsePlatform/parse-server/pull/919) (Marco129)
|
||||
* Improvement: Move DatabaseController and Schema fully to adaptive mongo collection. [\#909](https://github.com/ParsePlatform/parse-server/pull/909) (nlutsenko)
|
||||
* Improvement: Cleanup PushController/PushRouter, remove raw mongo collection access. [\#903](https://github.com/ParsePlatform/parse-server/pull/903) (nlutsenko)
|
||||
* Improvement: Increment badge the right way [\#902](https://github.com/ParsePlatform/parse-server/pull/902) (flovilmart)
|
||||
* Improvement: Migrate ParseGlobalConfig to new database storage API. [\#901](https://github.com/ParsePlatform/parse-server/pull/901) (nlutsenko)
|
||||
* Improvement: Improve delete flow for non-existent \_Join collection [\#881](https://github.com/ParsePlatform/parse-server/pull/881) (Marco129)
|
||||
* Improvement: Adding a role scenario test for issue 827 [\#878](https://github.com/ParsePlatform/parse-server/pull/878) (gfosco)
|
||||
* Improvement: Test empty authData block on login for \#413 [\#863](https://github.com/ParsePlatform/parse-server/pull/863) (gfosco)
|
||||
* Improvement: Modified the npm dev script to support Windows [\#846](https://github.com/ParsePlatform/parse-server/pull/846) (aneeshd16)
|
||||
* Improvement: Move HooksController to use MongoCollection instead of direct Mongo access. [\#844](https://github.com/ParsePlatform/parse-server/pull/844) (nlutsenko)
|
||||
* Improvement: Adds public\_html and views for packaging [\#839](https://github.com/ParsePlatform/parse-server/pull/839) (flovilmart)
|
||||
* Improvement: Better support for windows builds [\#831](https://github.com/ParsePlatform/parse-server/pull/831) (flovilmart)
|
||||
* Improvement: Convert Schema.js to ES6 class. [\#826](https://github.com/ParsePlatform/parse-server/pull/826) (nlutsenko)
|
||||
* Improvement: Remove duplicated instructions [\#816](https://github.com/ParsePlatform/parse-server/pull/816) (hramos)
|
||||
* Improvement: Completely migrate SchemasRouter to new MongoCollection API. [\#794](https://github.com/ParsePlatform/parse-server/pull/794) (nlutsenko)
|
||||
* Fix: Do not require where clause in $dontSelect condition on queries. [\#925](https://github.com/ParsePlatform/parse-server/pull/925) (nlutsenko)
|
||||
* Fix: Make sure that ACLs propagate to before/after save hooks. [\#924](https://github.com/ParsePlatform/parse-server/pull/924) (nlutsenko)
|
||||
* Fix: Support params option in Parse.Cloud.httpRequest. [\#912](https://github.com/ParsePlatform/parse-server/pull/912) (carmenlau)
|
||||
* Fix: Fix flaky Parse.GeoPoint test. [\#908](https://github.com/ParsePlatform/parse-server/pull/908) (nlutsenko)
|
||||
* Fix: Handle legacy \_client\_permissions key in \_SCHEMA. [\#900](https://github.com/ParsePlatform/parse-server/pull/900) (drew-gross)
|
||||
* Fix: Fixes bug when querying equalTo on objectId and relation [\#887](https://github.com/ParsePlatform/parse-server/pull/887) (flovilmart)
|
||||
* Fix: Allow crossdomain on filesRouter [\#876](https://github.com/ParsePlatform/parse-server/pull/876) (flovilmart)
|
||||
* Fix: Remove limit when counting results. [\#867](https://github.com/ParsePlatform/parse-server/pull/867) (gfosco)
|
||||
* Fix: beforeSave changes should propagate to the response [\#865](https://github.com/ParsePlatform/parse-server/pull/865) (gfosco)
|
||||
* Fix: Delete relation field when \_Join collection not exist [\#864](https://github.com/ParsePlatform/parse-server/pull/864) (Marco129)
|
||||
* Fix: Related query on non-existing column [\#861](https://github.com/ParsePlatform/parse-server/pull/861) (gfosco)
|
||||
* Fix: Update markdown in .github/ISSUE\_TEMPLATE.md [\#859](https://github.com/ParsePlatform/parse-server/pull/859) (igorshubovych)
|
||||
* Fix: Issue with creating wrong \_Session for Facebook login [\#857](https://github.com/ParsePlatform/parse-server/pull/857) (tobernguyen)
|
||||
* Fix: Leak warnings in tests, use mongodb-runner from node\_modules [\#843](https://github.com/ParsePlatform/parse-server/pull/843) (drew-gross)
|
||||
* Fix: Reversed roles lookup [\#841](https://github.com/ParsePlatform/parse-server/pull/841) (flovilmart)
|
||||
* Fix: Improves loading of Push Adapter, fix loading of S3Adapter [\#833](https://github.com/ParsePlatform/parse-server/pull/833) (flovilmart)
|
||||
* Fix: Add field to system schema [\#828](https://github.com/ParsePlatform/parse-server/pull/828) (Marco129)
|
||||
|
||||
### 2.1.4 (3/3/2016)
|
||||
|
||||
* New: serverInfo endpoint that returns server version and info about the server's features
|
||||
* Improvement: Add support for badges on iOS
|
||||
* Improvement: Improve failure handling in cloud code http requests
|
||||
* Improvement: Add support for queries on pointers and relations
|
||||
* Improvement: Add support for multiple $in clauses in a query
|
||||
* Improvement: Add allowClientClassCreation config option
|
||||
* Improvement: Allow atomically setting subdocument keys
|
||||
* Improvement: Allow arbitrarily deeply nested roles
|
||||
* Improvement: Set proper content-type in S3 File Adapter
|
||||
* Improvement: S3 adapter auto-creates buckets
|
||||
* Improvement: Better error messages for many errors
|
||||
* Performance: Improved algorithm for validating client keys
|
||||
* Experimental: Parse Hooks and Hooks API
|
||||
* Experimental: Email verification and password reset emails
|
||||
* Experimental: Improve compatability of logs feature with Parse.com
|
||||
* Fix: Fix for attempting to delete missing classes via schemas API
|
||||
* Fix: Allow creation of system classes via schemas API
|
||||
* Fix: Allow missing where cause in $select
|
||||
* Fix: Improve handling of invalid object ids
|
||||
* Fix: Replace query overwriting existing query
|
||||
* Fix: Propagate installationId in cloud code triggers
|
||||
* Fix: Session expiresAt is now a Date instead of a string
|
||||
* Fix: Fix count queries
|
||||
* Fix: Disallow _Role objects without names or without ACL
|
||||
* Fix: Better handling of invalid types submitted
|
||||
* Fix: beforeSave will not be triggered for attempts to save with invalid authData
|
||||
* Fix: Fix duplicate device token issues on Android
|
||||
* Fix: Allow empty authData on signup
|
||||
* Fix: Allow Master Key Headers (CORS)
|
||||
* Fix: Fix bugs if JavaScript key was not provided in server configuration
|
||||
* Fix: Parse Files on objects can now be stored without URLs
|
||||
* Fix: allow both objectId or installationId when modifying installation
|
||||
* Fix: Command line works better when not given options
|
||||
|
||||
### 2.1.3 (2/24/2016)
|
||||
|
||||
* Feature: Add initial support for in-app purchases
|
||||
* Feature: Better error messages when attempting to run the server on a port that is already in use or without a server URL
|
||||
* Feature: Allow customization of max file size
|
||||
* Performance: Faster saves if not using beforeSave triggers
|
||||
* Fix: Send session token in response to current user endpoint
|
||||
* Fix: Remove triggers for _Session collection
|
||||
* Fix: Improve compatability of cloud code beforeSave hook for newly created object
|
||||
* Fix: ACL creation for master key only objects
|
||||
* Fix: Allow uploading files without Content-Type
|
||||
* Fix: Add features to http requrest to match Parse.com
|
||||
* Fix: Bugs in development script when running from locations other than project root
|
||||
* Fix: Can pass query constraints in URL
|
||||
* Fix: Objects with legacy "_tombstone" key now don't cause issues.
|
||||
* Fix: Allow nested keys in objects to begin with underscores
|
||||
* Fix: Allow correct headers for CORS
|
||||
|
||||
### 2.1.2 (2/19/2016)
|
||||
|
||||
* Change: The S3 file adapter constructor requires a bucket name
|
||||
* Fix: Parse Query should throw if improperly encoded
|
||||
* Fix: Issue where roles were not used in some requests
|
||||
* Fix: serverURL will no longer default to api.parse.com/1
|
||||
|
||||
### 2.1.1 (2/18/2016)
|
||||
|
||||
* Experimental: Schemas API support for DELETE operations
|
||||
* Fix: Session token issue fetching Users
|
||||
* Fix: Facebook auth validation
|
||||
* Fix: Invalid error when deleting missing session
|
||||
|
||||
### 2.1.0 (2/17/2016)
|
||||
|
||||
* Feature: Support for additional OAuth providers
|
||||
@@ -9,7 +219,7 @@
|
||||
* Feature: Support for logs, extensible via Log Adapter
|
||||
* Feature: New Push Adapter for sending push notifications through OneSignal
|
||||
* Feature: Tighter default security for Users
|
||||
* Feature: Pass parameters to Cloud Code in query string
|
||||
* Feature: Pass parameters to cloud code in query string
|
||||
* Feature: Disable anonymous users via configuration.
|
||||
* Experimental: Schemas API support for PUT operations
|
||||
* Fix: Prevent installation ID from being added to User
|
||||
@@ -26,7 +236,7 @@
|
||||
### 2.0.8 (2/11/2016)
|
||||
|
||||
* Add: support for Android and iOS push notifications
|
||||
* Experimental: Cloud Code validation hooks (can mark as non-experimental after we have docs)
|
||||
* Experimental: cloud code validation hooks (can mark as non-experimental after we have docs)
|
||||
* Experimental: support for schemas API (GET and POST only)
|
||||
* Experimental: support for Parse Config (GET and POST only)
|
||||
* Fix: Querying objects with equality constraint on array column
|
||||
|
||||
+1
-4
@@ -12,7 +12,4 @@ We really want Parse to be yours, to see it grow and thrive in the open source c
|
||||
|
||||
##### Code of Conduct
|
||||
|
||||
This project adheres to the [Open Code of Conduct][code-of-conduct]. By participating, you are expected to honor this code.
|
||||
[code-of-conduct]: http://todogroup.org/opencodeofconduct/#Parse Server/fjm@fb.com
|
||||
|
||||
|
||||
This project adheres to the [Open Code of Conduct](http://todogroup.org/opencodeofconduct/#Parse Server/fjm@fb.com). By participating, you are expected to honor this code.
|
||||
|
||||
@@ -1,194 +1,266 @@
|
||||
## parse-server
|
||||

|
||||
|
||||
[](https://travis-ci.org/ParsePlatform/parse-server)
|
||||
[](https://codecov.io/github/ParsePlatform/parse-server?branch=master)
|
||||
[](https://www.npmjs.com/package/parse-server)
|
||||
|
||||
A Parse.com API compatible router package for Express
|
||||
Parse Server is an [open source version of the Parse backend](http://blog.parse.com/announcements/introducing-parse-server-and-the-database-migration-tool/) that can be deployed to any infrastructure that can run Node.js.
|
||||
|
||||
Read the announcement blog post here: http://blog.parse.com/announcements/introducing-parse-server-and-the-database-migration-tool/
|
||||
Parse Server works with the Express web application framework. It can be added to existing web applications, or run by itself.
|
||||
|
||||
Read the migration guide here: https://parse.com/docs/server/guide#migrating
|
||||
# Getting Started
|
||||
|
||||
There is a development wiki here on GitHub: https://github.com/ParsePlatform/parse-server/wiki
|
||||
The fastest and easiest way to get started is to run MongoDB and Parse Server locally.
|
||||
|
||||
We also have an [example project](https://github.com/ParsePlatform/parse-server-example) using the parse-server module on Express.
|
||||
|
||||
---
|
||||
|
||||
#### Basic options:
|
||||
|
||||
* databaseURI (required) - The connection string for your database, i.e. `mongodb://user:pass@host.com/dbname`
|
||||
* appId (required) - The application id to host with this server instance
|
||||
* masterKey (required) - The master key to use for overriding ACL security
|
||||
* cloud - The absolute path to your cloud code main.js file
|
||||
* fileKey - For migrated apps, this is necessary to provide access to files already hosted on Parse.
|
||||
* facebookAppIds - An array of valid Facebook application IDs.
|
||||
* serverURL - URL which will be used by Cloud Code functions to make requests against.
|
||||
* push - Configuration options for APNS and GCM push. See the [wiki entry](https://github.com/ParsePlatform/parse-server/wiki/Push).
|
||||
|
||||
#### Client key options:
|
||||
|
||||
The client keys used with Parse are no longer necessary with parse-server. If you wish to still require them, perhaps to be able to refuse access to older clients, you can set the keys at initialization time. Setting any of these keys will require all requests to provide one of the configured keys.
|
||||
|
||||
* clientKey
|
||||
* javascriptKey
|
||||
* restAPIKey
|
||||
* dotNetKey
|
||||
|
||||
#### OAuth Support
|
||||
|
||||
parse-server supports 3rd party authentication with
|
||||
|
||||
* Twitter
|
||||
* Meetup
|
||||
* Linkedin
|
||||
* Google
|
||||
* Instagram
|
||||
* Facebook
|
||||
|
||||
|
||||
Configuration options for these 3rd-party modules is done with the oauth option passed to ParseServer:
|
||||
## Running Parse Server locally
|
||||
|
||||
```
|
||||
{
|
||||
oauth: {
|
||||
twitter: {
|
||||
consumer_key: "", // REQUIRED
|
||||
consumer_secret: "" // REQUIRED
|
||||
},
|
||||
facebook: {
|
||||
appIds: "FACEBOOK APP ID"
|
||||
}
|
||||
}
|
||||
$ npm install -g parse-server mongodb-runner
|
||||
$ mongodb-runner start
|
||||
$ parse-server --appId APPLICATION_ID --masterKey MASTER_KEY
|
||||
```
|
||||
|
||||
You can use any arbitrary string as your application id and master key. These will be used by your clients to authenticate with the Parse Server.
|
||||
|
||||
That's it! You are now running a standalone version of Parse Server on your machine.
|
||||
|
||||
**Using a remote MongoDB?** Pass the `--databaseURI DATABASE_URI` parameter when starting `parse-server`. Learn more about configuring Parse Server [here](#configuration). For a full list of available options, run `parse-server --help`.
|
||||
|
||||
### Saving your first object
|
||||
|
||||
Now that you're running Parse Server, it is time to save your first object. We'll use the [REST API](https://parse.com/docs/rest/guide), but you can easily do the same using any of the [Parse SDKs](https://parseplatform.github.io/#sdks). Run the following:
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "X-Parse-Application-Id: APPLICATION_ID" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"score":1337,"playerName":"Sean Plott","cheatMode":false}' \
|
||||
http://localhost:1337/parse/classes/GameScore
|
||||
```
|
||||
|
||||
You should get a response similar to this:
|
||||
|
||||
```js
|
||||
{
|
||||
"objectId": "2ntvSpRGIK",
|
||||
"createdAt": "2016-03-11T23:51:48.050Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Custom Authentication
|
||||
|
||||
It is possible to leverage the OAuth support with any 3rd party authentication that you bring in.
|
||||
You can now retrieve this object directly (make sure to replace `2ntvSpRGIK` with the actual `objectId` you received when the object was created):
|
||||
|
||||
```bash
|
||||
$ curl -X GET \
|
||||
-H "X-Parse-Application-Id: APPLICATION_ID" \
|
||||
http://localhost:1337/parse/classes/GameScore/2ntvSpRGIK
|
||||
```
|
||||
```json
|
||||
// Response
|
||||
{
|
||||
|
||||
oauth: {
|
||||
my_custom_auth: {
|
||||
module: "PATH_TO_MODULE" // OR object,
|
||||
option1: "",
|
||||
option2: "",
|
||||
}
|
||||
}
|
||||
"objectId": "2ntvSpRGIK",
|
||||
"score": 1337,
|
||||
"playerName": "Sean Plott",
|
||||
"cheatMode": false,
|
||||
"updatedAt": "2016-03-11T23:51:48.050Z",
|
||||
"createdAt": "2016-03-11T23:51:48.050Z"
|
||||
}
|
||||
```
|
||||
|
||||
On this module, you need to implement and export those two functions `validateAuthData(authData, options) {} ` and `validateAppId(appIds, authData) {}`.
|
||||
Keeping tracks of individual object ids is not ideal, however. In most cases you will want to run a query over the collection, like so:
|
||||
|
||||
For more informations about custom auth please see the examples:
|
||||
```
|
||||
$ curl -X GET \
|
||||
-H "X-Parse-Application-Id: APPLICATION_ID" \
|
||||
http://localhost:1337/parse/classes/GameScore
|
||||
```
|
||||
```json
|
||||
// The response will provide all the matching objects within the `results` array:
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"objectId": "2ntvSpRGIK",
|
||||
"score": 1337,
|
||||
"playerName": "Sean Plott",
|
||||
"cheatMode": false,
|
||||
"updatedAt": "2016-03-11T23:51:48.050Z",
|
||||
"createdAt": "2016-03-11T23:51:48.050Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
- [facebook OAuth](https://github.com/ParsePlatform/parse-server/blob/master/src/oauth/facebook.js)
|
||||
- [twitter OAuth](https://github.com/ParsePlatform/parse-server/blob/master/src/oauth/twitter.js)
|
||||
- [instagram OAuth](https://github.com/ParsePlatform/parse-server/blob/master/src/oauth/instagram.js)
|
||||
```
|
||||
|
||||
To learn more about using saving and querying objects on Parse Server, check out the [Parse documentation](https://parse.com/docs).
|
||||
|
||||
#### Advanced options:
|
||||
### Connect your app to Parse Server
|
||||
|
||||
* filesAdapter - The default behavior (GridStore) can be changed by creating an adapter class (see [`FilesAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Files/FilesAdapter.js))
|
||||
* databaseAdapter (unfinished) - The backing store can be changed by creating an adapter class (see `DatabaseAdapter.js`)
|
||||
* loggerAdapter - The default behavior/transport (File) can be changed by creating an adapter class (see [`LoggerAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Logger/LoggerAdapter.js))
|
||||
* enableAnonymousUsers - Defaults to true. Set to false to disable anonymous users.
|
||||
---
|
||||
Parse provides SDKs for all the major platforms. Refer to the Parse Server guide to [learn how to connect your app to Parse Server](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#using-parse-sdks-with-parse-server).
|
||||
|
||||
### Usage
|
||||
## Running Parse Server elsewhere
|
||||
|
||||
You can create an instance of ParseServer, and mount it on a new or existing Express website:
|
||||
Once you have a better understanding of how the project works, please refer to the [Parse Server wiki](https://github.com/ParsePlatform/parse-server/wiki) for in-depth guides to deploy Parse Server to major infrastructure providers. Read on to learn more about additional ways of running Parse Server.
|
||||
|
||||
### Parse Server Sample Application
|
||||
|
||||
We have provided a basic [Node.js application](https://github.com/ParsePlatform/parse-server-example) that uses the Parse Server module on Express and can be easily deployed to various infrastructure providers:
|
||||
|
||||
* [Heroku and mLab](https://devcenter.heroku.com/articles/deploying-a-parse-server-to-heroku)
|
||||
* [AWS and Elastic Beanstalk](http://mobile.awsblog.com/post/TxCD57GZLM2JR/How-to-set-up-Parse-Server-on-AWS-using-AWS-Elastic-Beanstalk)
|
||||
* [Digital Ocean](https://www.digitalocean.com/community/tutorials/how-to-run-parse-server-on-ubuntu-14-04)
|
||||
* [NodeChef](https://nodechef.com/blog/post/6/migrate-from-parse-to-nodechef%E2%80%99s-managed-parse-server)
|
||||
* [Google App Engine](https://medium.com/@justinbeckwith/deploying-parse-server-to-google-app-engine-6bc0b7451d50)
|
||||
* [Microsoft Azure](https://azure.microsoft.com/en-us/blog/azure-welcomes-parse-developers/)
|
||||
* [Pivotal Web Services](https://github.com/cf-platform-eng/pws-parse-server)
|
||||
* [Back4app](http://blog.back4app.com/2016/03/01/quick-wizard-migration/)
|
||||
|
||||
### Parse Server + Express
|
||||
|
||||
You can also create an instance of Parse Server, and mount it on a new or existing Express website:
|
||||
|
||||
```js
|
||||
var express = require('express');
|
||||
var ParseServer = require('parse-server').ParseServer;
|
||||
|
||||
var app = express();
|
||||
|
||||
var port = process.env.PORT || 1337;
|
||||
|
||||
// Specify the connection string for your mongodb database
|
||||
// and the location to your Parse cloud code
|
||||
var api = new ParseServer({
|
||||
databaseURI: 'mongodb://localhost:27017/dev',
|
||||
cloud: '/home/myApp/cloud/main.js', // Provide an absolute path
|
||||
databaseURI: 'mongodb://localhost:27017/dev', // Connection string for your MongoDB database
|
||||
cloud: '/home/myApp/cloud/main.js', // Absolute path to your Cloud Code
|
||||
appId: 'myAppId',
|
||||
masterKey: '', //Add your master key here. Keep it secret!
|
||||
masterKey: 'myMasterKey', // Keep this key secret!
|
||||
fileKey: 'optionalFileKey',
|
||||
serverURL: 'http://localhost:' + port + '/parse' // Don't forget to change to https if needed
|
||||
serverURL: 'http://localhost:1337/parse' // Don't forget to change to https if needed
|
||||
});
|
||||
|
||||
// Serve the Parse API on the /parse URL prefix
|
||||
app.use('/parse', api);
|
||||
|
||||
// Hello world
|
||||
app.get('/', function(req, res) {
|
||||
res.status(200).send('Express is running here.');
|
||||
app.listen(1337, function() {
|
||||
console.log('parse-server-example running on port 1337.');
|
||||
});
|
||||
|
||||
app.listen(port, function() {
|
||||
console.log('parse-server-example running on port ' + port + '.');
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
For a full list of available options, run `parse-server --help`.
|
||||
|
||||
#### Standalone usage
|
||||
# Documentation
|
||||
|
||||
You can configure the Parse Server with environment variables:
|
||||
The full documentation for Parse Server is available in the [wiki](https://github.com/ParsePlatform/parse-server/wiki). The [Parse Server guide](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide) is a good place to get started. If you're interested in developing for Parse Server, the [Development guide](https://github.com/ParsePlatform/parse-server/wiki/Development-Guide) will help you get set up.
|
||||
|
||||
```js
|
||||
## Migrating an Existing Parse App
|
||||
|
||||
The hosted version of Parse will be fully retired on January 28th, 2017. If you are planning to migrate an app, you need to begin work as soon as possible. There are a few areas where Parse Server does not provide compatibility with the hosted version of Parse. Learn more in the [Migration guide](https://github.com/ParsePlatform/parse-server/wiki/Migrating-an-Existing-Parse-App).
|
||||
|
||||
## Configuration
|
||||
|
||||
Parse Server can be configured using the following options. You may pass these as parameters when running a standalone `parse-server`, or by loading a configuration file in JSON format using `parse-server path/to/configuration.json`. If you're using Parse Server on Express, you may also pass these to the `ParseServer` object as options.
|
||||
|
||||
For the full list of available options, run `parse-server --help`.
|
||||
|
||||
#### Basic options
|
||||
|
||||
* `appId` **(required)** - The application id to host with this server instance. You can use any arbitrary string. For migrated apps, this should match your hosted Parse app.
|
||||
* `masterKey` **(required)** - The master key to use for overriding ACL security. You can use any arbitrary string. Keep it secret! For migrated apps, this should match your hosted Parse app.
|
||||
* `databaseURI` **(required)** - The connection string for your database, i.e. `mongodb://user:pass@host.com/dbname`.
|
||||
* `port` - The default port is 1337, specify this parameter to use a different port.
|
||||
* `serverURL` - URL to your Parse Server (don't forget to specify http:// or https://). This URL will be used when making requests to Parse Server from Cloud Code.
|
||||
* `cloud` - The absolute path to your cloud code `main.js` file.
|
||||
* `push` - Configuration options for APNS and GCM push. See the [Push Notifications wiki entry](https://github.com/ParsePlatform/parse-server/wiki/Push).
|
||||
|
||||
#### Client key options
|
||||
|
||||
The client keys used with Parse are no longer necessary with Parse Server. If you wish to still require them, perhaps to be able to refuse access to older clients, you can set the keys at initialization time. Setting any of these keys will require all requests to provide one of the configured keys.
|
||||
|
||||
* `clientKey`
|
||||
* `javascriptKey`
|
||||
* `restAPIKey`
|
||||
* `dotNetKey`
|
||||
|
||||
#### Advanced options
|
||||
|
||||
* `fileKey` - For migrated apps, this is necessary to provide access to files already hosted on Parse.
|
||||
* `allowClientClassCreation` - Set to false to disable client class creation. Defaults to true.
|
||||
* `enableAnonymousUsers` - Set to false to disable anonymous users. Defaults to true.
|
||||
* `oauth` - Used to configure support for [3rd party authentication](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#oauth).
|
||||
* `facebookAppIds` - An array of valid Facebook application IDs that users may authenticate with.
|
||||
* `mountPath` - Mount path for the server. Defaults to `/parse`.
|
||||
* `filesAdapter` - The default behavior (GridStore) can be changed by creating an adapter class (see [`FilesAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Files/FilesAdapter.js)).
|
||||
* `maxUploadSize` - Max file size for uploads. Defaults to 20 MB.
|
||||
* `loggerAdapter` - The default behavior/transport (File) can be changed by creating an adapter class (see [`LoggerAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Logger/LoggerAdapter.js)).
|
||||
* `sessionLength` - The length of time in seconds that a session should be valid for. Defaults to 31536000 seconds (1 year).
|
||||
|
||||
##### Email verification and password reset
|
||||
|
||||
Verifying user email addresses and enabling password reset via email requries an email adapter. As part of the `parse-server` package we provide an adapter for sending email through Mailgun. To use it, sign up for Mailgun, and add this to your initialization code:
|
||||
|
||||
```js
|
||||
var server = ParseServer({
|
||||
...otherOptions,
|
||||
// Enable email verification
|
||||
verifyUserEmails: true,
|
||||
// The public URL of your app.
|
||||
// This will appear in the link that is used to verify email addresses and reset passwords.
|
||||
publicServerURL: 'https://example.com',
|
||||
// Your apps name. This will appear in the subject and body of the emails that are sent.
|
||||
appName: 'Parse App',
|
||||
// The email adapter
|
||||
emailAdapter: {
|
||||
module: 'parse-server-simple-mailgun-adapter',
|
||||
options: {
|
||||
// The address that your emails come from
|
||||
fromAddress: 'parse@example.com',
|
||||
// Your domain from mailgun.com
|
||||
domain: 'example.com',
|
||||
// Your API key from mailgun.com
|
||||
apiKey: 'key-mykey',
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
You can also use other email adapters contributed by the community such as [parse-server-sendgrid-adapter](https://www.npmjs.com/package/parse-server-sendgrid-adapter).
|
||||
|
||||
### Using environment variables to configure Parse Server
|
||||
|
||||
You may configure the Parse Server using environment variables:
|
||||
|
||||
```bash
|
||||
PORT
|
||||
PARSE_SERVER_APPLICATION_ID
|
||||
PARSE_SERVER_MASTER_KEY
|
||||
PARSE_SERVER_DATABASE_URI
|
||||
PARSE_SERVER_URL
|
||||
PARSE_SERVER_CLOUD_CODE_MAIN
|
||||
PARSE_SERVER_COLLECTION_PREFIX
|
||||
PARSE_SERVER_APPLICATION_ID // required
|
||||
PARSE_SERVER_CLIENT_KEY
|
||||
PARSE_SERVER_REST_API_KEY
|
||||
PARSE_SERVER_DOTNET_KEY
|
||||
PARSE_SERVER_JAVASCRIPT_KEY
|
||||
PARSE_SERVER_DOTNET_KEY
|
||||
PARSE_SERVER_MASTER_KEY // required
|
||||
PARSE_SERVER_FILE_KEY
|
||||
PARSE_SERVER_FACEBOOK_APP_IDS // string of comma separated list
|
||||
|
||||
```
|
||||
|
||||
The default port is 1337, to use a different port set the PORT environment variable:
|
||||
|
||||
```bash
|
||||
$ PORT=8080 parse-server --appId APPLICATION_ID --masterKey MASTER_KEY
|
||||
```
|
||||
|
||||
Alernatively, you can use the `PARSE_SERVER_OPTIONS` environment variable set to the JSON of your configuration (see Usage).
|
||||
For the full list of configurable environment variables, run `parse-server --help`.
|
||||
|
||||
To start the server, just run `npm start`.
|
||||
### Configuring File Adapters
|
||||
|
||||
##### Global installation
|
||||
Parse Server allows developers to choose from several options when hosting files:
|
||||
|
||||
You can install parse-server globally
|
||||
* `GridStoreAdapter`, which is backed by MongoDB;
|
||||
* `S3Adapter`, which is backed by [Amazon S3](https://aws.amazon.com/s3/); or
|
||||
* `GCSAdapter`, which is backed by [Google Cloud Storage](https://cloud.google.com/storage/)
|
||||
|
||||
`$ npm install -g parse-server`
|
||||
`GridStoreAdapter` is used by default and requires no setup, but if you're interested in using S3 or Google Cloud Storage, additional configuration information is available in the [Parse Server wiki](https://github.com/ParsePlatform/parse-server/wiki/Configuring-File-Adapters).
|
||||
|
||||
Now you can just run `$ parse-server` from your command line.
|
||||
# Support
|
||||
|
||||
For implementation related questions or technical support, please refer to the [Stack Overflow](http://stackoverflow.com/questions/tagged/parse.com) and [Server Fault](https://serverfault.com/tags/parse) communities.
|
||||
|
||||
### Supported
|
||||
If you believe you've found an issue with Parse Server, make sure these boxes are checked before [reporting an issue](https://github.com/ParsePlatform/parse-server/issues):
|
||||
|
||||
* CRUD operations
|
||||
* Schema validation
|
||||
* Pointers
|
||||
* Users, including Facebook login and anonymous users
|
||||
* Files
|
||||
* Push Notifications - See the [wiki entry](https://github.com/ParsePlatform/parse-server/wiki/Push).
|
||||
* Installations
|
||||
* Sessions
|
||||
* Geopoints
|
||||
* Roles
|
||||
* Class-level Permissions (see below)
|
||||
- [ ] You've met the [prerequisites](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites).
|
||||
|
||||
Parse server does not include a web-based dashboard, which is where class-level permissions have always been configured. If you migrate an app from Parse, you'll see the format for CLPs in the SCHEMA collection. There is also a `setPermissions` method on the `Schema` class, which you can see used in the unit-tests in `Schema.spec.js`
|
||||
You can also set up an app on Parse, providing the connection string for your mongo database, and continue to use the dashboard on Parse.com.
|
||||
- [ ] You're running the [latest version](https://github.com/ParsePlatform/parse-server/releases) of Parse Server.
|
||||
|
||||
### Not supported
|
||||
- [ ] You've searched through [existing issues](https://github.com/ParsePlatform/parse-server/issues?utf8=%E2%9C%93&q=). Chances are that your issue has been reported or resolved before.
|
||||
|
||||
* `Parse.User.current()` or `Parse.Cloud.useMasterKey()` in cloud code. Instead of `Parse.User.current()` use `request.user` and instead of `Parse.Cloud.useMasterKey()` pass `useMasterKey: true` to each query. To make queries and writes as a specific user within Cloud Code, you need the user's session token, which is available in `request.user.getSessionToken()`.
|
||||
# Contributing
|
||||
|
||||
We really want Parse to be yours, to see it grow and thrive in the open source community. Please see the [Contributing to Parse Server guide](CONTRIBUTING.md).
|
||||
|
||||
@@ -4,14 +4,17 @@ var nodemon = require('nodemon');
|
||||
var babel = require("babel-core");
|
||||
var gaze = require('gaze');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
// Watch the src and transpile when changed
|
||||
gaze('src/**/*', function(err, watcher) {
|
||||
if (err) throw err;
|
||||
watcher.on('changed', function(file) {
|
||||
console.log(file + " has changed");
|
||||
watcher.on('changed', function(sourceFile) {
|
||||
console.log(sourceFile + " has changed");
|
||||
try {
|
||||
fs.writeFile(file.replace(/\/src\//, "/lib/"), babel.transformFileSync(file).code);
|
||||
targetFile = path.relative(__dirname, sourceFile).replace(/\/src\//, '/lib/');
|
||||
targetFile = path.resolve(__dirname, targetFile);
|
||||
fs.writeFile(targetFile, babel.transformFileSync(sourceFile).code);
|
||||
} catch (e) {
|
||||
console.error(e.message, e.stack);
|
||||
}
|
||||
|
||||
+1
-45
@@ -1,47 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
var express = require('express');
|
||||
var ParseServer = require("../lib/index").ParseServer;
|
||||
|
||||
var app = express();
|
||||
|
||||
var options = {};
|
||||
if (process.env.PARSE_SERVER_OPTIONS) {
|
||||
|
||||
options = JSON.parse(process.env.PARSE_SERVER_OPTIONS);
|
||||
|
||||
} else {
|
||||
|
||||
options.databaseURI = process.env.PARSE_SERVER_DATABASE_URI;
|
||||
options.cloud = process.env.PARSE_SERVER_CLOUD_CODE_MAIN;
|
||||
options.collectionPrefix = process.env.PARSE_SERVER_COLLECTION_PREFIX;
|
||||
|
||||
// Keys and App ID
|
||||
options.appId = process.env.PARSE_SERVER_APPLICATION_ID;
|
||||
options.clientKey = process.env.PARSE_SERVER_CLIENT_KEY;
|
||||
options.restAPIKey = process.env.PARSE_SERVER_REST_API_KEY;
|
||||
options.dotNetKey = process.env.PARSE_SERVER_DOTNET_KEY;
|
||||
options.javascriptKey = process.env.PARSE_SERVER_JAVASCRIPT_KEY;
|
||||
options.masterKey = process.env.PARSE_SERVER_MASTER_KEY;
|
||||
options.fileKey = process.env.PARSE_SERVER_FILE_KEY;
|
||||
// Comma separated list of facebook app ids
|
||||
var facebookAppIds = process.env.PARSE_SERVER_FACEBOOK_APP_IDS;
|
||||
|
||||
if (facebookAppIds) {
|
||||
facebookAppIds = facebookAppIds.split(",");
|
||||
options.facebookAppIds = facebookAppIds;
|
||||
}
|
||||
|
||||
var oauth = process.env.PARSE_SERVER_OAUTH_PROVIDERS;
|
||||
if (oauth) {
|
||||
options.oauth = JSON.parse(oauth);
|
||||
};
|
||||
}
|
||||
|
||||
var mountPath = process.env.PARSE_SERVER_MOUNT_PATH || "/";
|
||||
var api = new ParseServer(options);
|
||||
app.use(mountPath, api);
|
||||
|
||||
var port = process.env.PORT || 1337;
|
||||
app.listen(port, function() {
|
||||
console.log('parse-server-example running on http://localhost:'+ port + mountPath);
|
||||
});
|
||||
require("../lib/cli/parse-server");
|
||||
|
||||
Executable
+163
@@ -0,0 +1,163 @@
|
||||
#!/bin/sh
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
BOLD='\033[1m'
|
||||
CHECK="${GREEN}\xE2\x9C\x93${NC}"
|
||||
DEFAULT_MONGODB_URI='mongodb://localhost:127.0.0.1:27017/parse'
|
||||
|
||||
confirm() {
|
||||
DEFAULT=$1;
|
||||
shift
|
||||
printf "$@"
|
||||
read -r YN
|
||||
if [ "$YN" = "" ] && [ "$DEFAULT" = 'N' ]; then
|
||||
exit 1
|
||||
elif [ "$YN" != "" ] && [ "$YN" != "y" ] && [ "$YN" != "Y" ]; then
|
||||
echo 'Bye Bye!'
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
genstring() {
|
||||
local l=$1
|
||||
[ "$l" == "" ] && l=40
|
||||
LC_ALL=C tr -dc A-Za-z0-9 < /dev/urandom | head -c ${l}
|
||||
}
|
||||
|
||||
check_node() {
|
||||
node=`which node 2>&1`
|
||||
ret=$?
|
||||
|
||||
if [ $ret -eq 0 ] && [ -x "$node" ]; then
|
||||
echo "${CHECK} node:" $(node -v)
|
||||
(exit 0)
|
||||
else
|
||||
echo "parse-server cannot be installed without Node.js." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_npm() {
|
||||
npm=`which npm 2>&1`
|
||||
ret=$?
|
||||
|
||||
if [ $ret -eq 0 ] && [ -x "$npm" ]; then
|
||||
echo "${CHECK} npm:" $(npm -v)
|
||||
(exit 0)
|
||||
else
|
||||
echo "parse-server cannot be installed without npm." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
echo ''
|
||||
echo 'This will setup parse-server in the current directory'
|
||||
confirm 'Y' 'Do you want to continue? (Y/n): '
|
||||
|
||||
check_node
|
||||
check_npm
|
||||
|
||||
echo "Setting up parse-server in $PWD"
|
||||
|
||||
if [ -f './package.json' ]; then
|
||||
echo "\n${RED}package.json exists${NC}"
|
||||
confirm 'N' "Do you want to continue? \n${RED}this will erase your configuration${NC} (y/N): "
|
||||
fi
|
||||
|
||||
|
||||
if [ -f 'config.json' ]; then
|
||||
echo "\n${RED}config.json exists${NC}"
|
||||
confirm 'N' "Do you want to continue? \n${RED}this will erase your configuration${NC} (y/N): "
|
||||
fi
|
||||
|
||||
APP_NAME=''
|
||||
i=0
|
||||
while [ "$APP_NAME" = "" ]
|
||||
do
|
||||
[[ $i != 0 ]] && printf "${RED}An application name is required!${NC}\n"
|
||||
printf 'Enter your Application Name: '
|
||||
read -r APP_NAME
|
||||
i=$(($i+1))
|
||||
done
|
||||
|
||||
printf 'Enter your appId (leave empty to generate): '
|
||||
read -r APP_ID
|
||||
|
||||
[[ $APP_ID = '' ]] && APP_ID=$(genstring) && printf "\n$APP_ID\n\n"
|
||||
|
||||
printf 'Enter your masterKey (leave empty to generate): '
|
||||
read -r MASTER_KEY
|
||||
|
||||
[[ $MASTER_KEY = '' ]] && MASTER_KEY=$(genstring) && printf "\n$MASTER_KEY\n\n"
|
||||
|
||||
printf "Enter your mongodbURI (%s): " $DEFAULT_MONGODB_URI
|
||||
read -r MONGODB_URI
|
||||
|
||||
[[ $MONGODB_URI = '' ]] && MONGODB_URI="$DEFAULT_MONGODB_URI"
|
||||
|
||||
cat > ./config.json << EOF
|
||||
{
|
||||
"appId": "$APP_ID",
|
||||
"masterKey": "$MASTER_KEY",
|
||||
"appName": "$APP_NAME",
|
||||
"cloud": "./cloud/main",
|
||||
"mongodbURI": "$MONGODB_URI"
|
||||
}
|
||||
EOF
|
||||
echo "${CHECK} Created config.json"
|
||||
|
||||
# Make a proper npm app name
|
||||
NPM_APP_NAME=$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
|
||||
cat > ./package.json << EOF
|
||||
{
|
||||
"name": "$NPM_APP_NAME",
|
||||
"scripts": {
|
||||
"start": "parse-server ./config.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"parse-server": "^2.0.0"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
echo "${CHECK} Created package.json"
|
||||
|
||||
if [ -d "./cloud/" ]; then
|
||||
echo "${CHECK} cloud/ exists"
|
||||
else
|
||||
mkdir -p ./cloud
|
||||
echo "${CHECK} Created cloud/"
|
||||
fi
|
||||
|
||||
if [ -e "./cloud/main.js" ]; then
|
||||
echo "${CHECK} cloud/main.js exists"
|
||||
else
|
||||
echo "${CHECK} Created cloud/main.js"
|
||||
cat > ./cloud/main.js << EOF
|
||||
// Cloud Code entry point
|
||||
|
||||
EOF
|
||||
fi
|
||||
|
||||
if [ -d "./public/" ]; then
|
||||
echo "${CHECK} public/ exists"
|
||||
else
|
||||
mkdir -p ./public
|
||||
echo "${CHECK} Created public/"
|
||||
fi
|
||||
|
||||
echo "\n${CHECK} running npm install\n"
|
||||
|
||||
npm install
|
||||
|
||||
CURL_CMD=$(cat << EOF
|
||||
curl -X POST -H 'X-Parse-Application-Id: ${APP_ID}' \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
-d '{"foo":"bar"}' http://localhost:1337/parse/classes/TestObject
|
||||
EOF)
|
||||
|
||||
echo "\n${CHECK} Happy Parsing!\n\n"
|
||||
echo "${CHECK} Make sure you have ${BOLD}mongo${NC} listening on ${BOLD}${MONGODB_URI}${NC}"
|
||||
echo "${CHECK} start parse-server by running ${BOLD}npm start${NC}"
|
||||
echo "${CHECK} Test your setup with:\n\n${CURL_CMD}\n"
|
||||
+38
-11
@@ -1,50 +1,77 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "2.1.0",
|
||||
"version": "2.2.6",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ParsePlatform/parse-server"
|
||||
},
|
||||
"files": [
|
||||
"bin/",
|
||||
"lib/",
|
||||
"public_html/",
|
||||
"views/",
|
||||
"LICENSE",
|
||||
"PATENTS",
|
||||
"README.md"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"apn": "^1.7.5",
|
||||
"aws-sdk": "~2.2.33",
|
||||
"babel-polyfill": "^6.5.0",
|
||||
"babel-runtime": "^6.5.0",
|
||||
"bcrypt-nodejs": "0.0.3",
|
||||
"body-parser": "^1.14.2",
|
||||
"colors": "^1.1.2",
|
||||
"commander": "^2.9.0",
|
||||
"deepcopy": "^0.6.1",
|
||||
"express": "^4.13.4",
|
||||
"intersect": "^1.0.1",
|
||||
"lru-cache": "^4.0.0",
|
||||
"mailgun-js": "^0.7.7",
|
||||
"mime": "^1.3.4",
|
||||
"mongodb": "~2.1.0",
|
||||
"multer": "^1.1.0",
|
||||
"node-gcm": "^0.14.0",
|
||||
"parse": "^1.7.0",
|
||||
"parse": "^1.8.0",
|
||||
"parse-server-fs-adapter": "^1.0.0",
|
||||
"parse-server-gcs-adapter": "^1.0.0",
|
||||
"parse-server-push-adapter": "^1.0.0",
|
||||
"parse-server-s3-adapter": "^1.0.0",
|
||||
"parse-server-simple-mailgun-adapter": "^1.0.0",
|
||||
"redis": "^2.5.0-1",
|
||||
"request": "^2.65.0",
|
||||
"winston": "^2.1.1"
|
||||
"tv4": "^1.2.7",
|
||||
"winston": "^2.1.1",
|
||||
"winston-daily-rotate-file": "^1.0.1",
|
||||
"ws": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.5.1",
|
||||
"babel-core": "^6.5.1",
|
||||
"babel-istanbul": "^0.6.0",
|
||||
"babel-plugin-syntax-flow": "^6.5.0",
|
||||
"babel-plugin-transform-flow-strip-types": "^6.5.0",
|
||||
"babel-preset-es2015": "^6.5.0",
|
||||
"babel-preset-stage-0": "^6.5.0",
|
||||
"babel-register": "^6.5.1",
|
||||
"codecov": "^1.0.1",
|
||||
"cross-env": "^1.0.7",
|
||||
"deep-diff": "^0.3.3",
|
||||
"flow-bin": "^0.22.0",
|
||||
"gaze": "^0.5.2",
|
||||
"jasmine": "^2.3.2",
|
||||
"mongodb-runner": "^3.1.15",
|
||||
"mongodb-runner": "3.1.15",
|
||||
"nodemon": "^1.8.1"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "npm run build && bin/dev",
|
||||
"dev": "npm run build && node bin/dev",
|
||||
"build": "./node_modules/.bin/babel src/ -d lib/",
|
||||
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.0.8} ./node_modules/.bin/mongodb-runner start",
|
||||
"test": "cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node ./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/** ./node_modules/jasmine/bin/jasmine.js",
|
||||
"posttest": "mongodb-runner stop",
|
||||
"start": "./bin/parse-server",
|
||||
"test": "cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node $COVERAGE_OPTION ./node_modules/jasmine/bin/jasmine.js",
|
||||
"test:win": "npm run pretest && cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node ./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/** ./node_modules/jasmine/bin/jasmine.js && npm run posttest",
|
||||
"posttest": "./node_modules/.bin/mongodb-runner stop",
|
||||
"coverage": "cross-env COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**' npm test",
|
||||
"start": "node ./bin/parse-server",
|
||||
"prepublish": "npm run build"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- This page is displayed when someone navigates to a verify email or reset password link
|
||||
but their security token is wrong. This can either mean the user has clicked on a
|
||||
stale link (i.e. re-click on a password reset link after resetting their password) or
|
||||
(rarely) this could be a sign of a malicious user trying to tamper with your app.
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title>Invalid Link</title>
|
||||
<style type='text/css'>
|
||||
.container {
|
||||
border-width: 0px;
|
||||
display: block;
|
||||
font: inherit;
|
||||
font-family: 'Helvetica Neue', Helvetica;
|
||||
font-size: 16px;
|
||||
height: 30px;
|
||||
line-height: 16px;
|
||||
margin: 45px 0px 0px 45px;
|
||||
padding: 0px 8px 0px 8px;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
color: #0067AB;
|
||||
display: block;
|
||||
font: inherit;
|
||||
font-family: 'Open Sans', 'Helvetica Neue', Helvetica;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
margin: 0 0 15px 0;
|
||||
padding: 0 0 0 0;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Invalid Link</h1>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!-- This page is displayed whenever someone has successfully reset their password.
|
||||
Pro and Enterprise accounts may edit this page and tell Parse to use that custom
|
||||
version in their Parse app. See the App Settigns page for more information.
|
||||
This page will be called with the query param 'username'
|
||||
-->
|
||||
<head>
|
||||
<title>Password Reset</title>
|
||||
<style type='text/css'>
|
||||
h1 {
|
||||
color: #0067AB;
|
||||
display: block;
|
||||
font: inherit;
|
||||
font-family: 'Open Sans', 'Helvetica Neue', Helvetica;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
margin: 45px 0px 0px 45px;
|
||||
padding: 0px 8px 0px 8px;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<h1>Successfully updated your password!</h1>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!-- This page is displayed whenever someone has successfully reset their password.
|
||||
Pro and Enterprise accounts may edit this page and tell Parse to use that custom
|
||||
version in their Parse app. See the App Settigns page for more information.
|
||||
This page will be called with the query param 'username'
|
||||
-->
|
||||
<head>
|
||||
<title>Email Verification</title>
|
||||
<style type='text/css'>
|
||||
h1 {
|
||||
color: #0067AB;
|
||||
display: block;
|
||||
font: inherit;
|
||||
font-family: 'Open Sans', 'Helvetica Neue', Helvetica;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
margin: 45px 0px 0px 45px;
|
||||
padding: 0px 8px 0px 8px;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<h1>Successfully verified your email!</h1>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,307 +0,0 @@
|
||||
var APNS = require('../src/APNS');
|
||||
|
||||
describe('APNS', () => {
|
||||
|
||||
it('can initialize with single cert', (done) => {
|
||||
var args = {
|
||||
cert: 'prodCert.pem',
|
||||
key: 'prodKey.pem',
|
||||
production: true,
|
||||
bundleId: 'bundleId'
|
||||
}
|
||||
var apns = new APNS(args);
|
||||
|
||||
expect(apns.conns.length).toBe(1);
|
||||
var apnsConnection = apns.conns[0];
|
||||
expect(apnsConnection.index).toBe(0);
|
||||
expect(apnsConnection.bundleId).toBe(args.bundleId);
|
||||
// TODO: Remove this checking onec we inject APNS
|
||||
var prodApnsOptions = apnsConnection.options;
|
||||
expect(prodApnsOptions.cert).toBe(args.cert);
|
||||
expect(prodApnsOptions.key).toBe(args.key);
|
||||
expect(prodApnsOptions.production).toBe(args.production);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can initialize with multiple certs', (done) => {
|
||||
var args = [
|
||||
{
|
||||
cert: 'devCert.pem',
|
||||
key: 'devKey.pem',
|
||||
production: false,
|
||||
bundleId: 'bundleId'
|
||||
},
|
||||
{
|
||||
cert: 'prodCert.pem',
|
||||
key: 'prodKey.pem',
|
||||
production: true,
|
||||
bundleId: 'bundleIdAgain'
|
||||
}
|
||||
]
|
||||
|
||||
var apns = new APNS(args);
|
||||
expect(apns.conns.length).toBe(2);
|
||||
var devApnsConnection = apns.conns[1];
|
||||
expect(devApnsConnection.index).toBe(1);
|
||||
var devApnsOptions = devApnsConnection.options;
|
||||
expect(devApnsOptions.cert).toBe(args[0].cert);
|
||||
expect(devApnsOptions.key).toBe(args[0].key);
|
||||
expect(devApnsOptions.production).toBe(args[0].production);
|
||||
expect(devApnsConnection.bundleId).toBe(args[0].bundleId);
|
||||
|
||||
var prodApnsConnection = apns.conns[0];
|
||||
expect(prodApnsConnection.index).toBe(0);
|
||||
// TODO: Remove this checking onec we inject APNS
|
||||
var prodApnsOptions = prodApnsConnection.options;
|
||||
expect(prodApnsOptions.cert).toBe(args[1].cert);
|
||||
expect(prodApnsOptions.key).toBe(args[1].key);
|
||||
expect(prodApnsOptions.production).toBe(args[1].production);
|
||||
expect(prodApnsOptions.bundleId).toBe(args[1].bundleId);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can generate APNS notification', (done) => {
|
||||
//Mock request data
|
||||
var data = {
|
||||
'alert': 'alert',
|
||||
'badge': 100,
|
||||
'sound': 'test',
|
||||
'content-available': 1,
|
||||
'category': 'INVITE_CATEGORY',
|
||||
'key': 'value',
|
||||
'keyAgain': 'valueAgain'
|
||||
};
|
||||
var expirationTime = 1454571491354
|
||||
|
||||
var notification = APNS.generateNotification(data, expirationTime);
|
||||
|
||||
expect(notification.alert).toEqual(data.alert);
|
||||
expect(notification.badge).toEqual(data.badge);
|
||||
expect(notification.sound).toEqual(data.sound);
|
||||
expect(notification.contentAvailable).toEqual(1);
|
||||
expect(notification.category).toEqual(data.category);
|
||||
expect(notification.payload).toEqual({
|
||||
'key': 'value',
|
||||
'keyAgain': 'valueAgain'
|
||||
});
|
||||
expect(notification.expiry).toEqual(expirationTime);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can choose conns for device without appIdentifier', (done) => {
|
||||
// Mock conns
|
||||
var conns = [
|
||||
{
|
||||
bundleId: 'bundleId'
|
||||
},
|
||||
{
|
||||
bundleId: 'bundleIdAgain'
|
||||
}
|
||||
];
|
||||
// Mock device
|
||||
var device = {};
|
||||
|
||||
var qualifiedConns = APNS.chooseConns(conns, device);
|
||||
expect(qualifiedConns).toEqual([0, 1]);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can choose conns for device with valid appIdentifier', (done) => {
|
||||
// Mock conns
|
||||
var conns = [
|
||||
{
|
||||
bundleId: 'bundleId'
|
||||
},
|
||||
{
|
||||
bundleId: 'bundleIdAgain'
|
||||
}
|
||||
];
|
||||
// Mock device
|
||||
var device = {
|
||||
appIdentifier: 'bundleId'
|
||||
};
|
||||
|
||||
var qualifiedConns = APNS.chooseConns(conns, device);
|
||||
expect(qualifiedConns).toEqual([0]);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can choose conns for device with invalid appIdentifier', (done) => {
|
||||
// Mock conns
|
||||
var conns = [
|
||||
{
|
||||
bundleId: 'bundleId'
|
||||
},
|
||||
{
|
||||
bundleId: 'bundleIdAgain'
|
||||
}
|
||||
];
|
||||
// Mock device
|
||||
var device = {
|
||||
appIdentifier: 'invalid'
|
||||
};
|
||||
|
||||
var qualifiedConns = APNS.chooseConns(conns, device);
|
||||
expect(qualifiedConns).toEqual([]);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can handle transmission error when notification is not in cache or device is missing', (done) => {
|
||||
// Mock conns
|
||||
var conns = [];
|
||||
var errorCode = 1;
|
||||
var notification = undefined;
|
||||
var device = {};
|
||||
|
||||
APNS.handleTransmissionError(conns, errorCode, notification, device);
|
||||
|
||||
var notification = {};
|
||||
var device = undefined;
|
||||
|
||||
APNS.handleTransmissionError(conns, errorCode, notification, device);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can handle transmission error when there are other qualified conns', (done) => {
|
||||
// Mock conns
|
||||
var conns = [
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId1'
|
||||
},
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId1'
|
||||
},
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId2'
|
||||
},
|
||||
];
|
||||
var errorCode = 1;
|
||||
var notification = {};
|
||||
var apnDevice = {
|
||||
connIndex: 0,
|
||||
appIdentifier: 'bundleId1'
|
||||
};
|
||||
|
||||
APNS.handleTransmissionError(conns, errorCode, notification, apnDevice);
|
||||
|
||||
expect(conns[0].pushNotification).not.toHaveBeenCalled();
|
||||
expect(conns[1].pushNotification).toHaveBeenCalled();
|
||||
expect(conns[2].pushNotification).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can handle transmission error when there is no other qualified conns', (done) => {
|
||||
// Mock conns
|
||||
var conns = [
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId1'
|
||||
},
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId1'
|
||||
},
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId1'
|
||||
},
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId2'
|
||||
},
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId1'
|
||||
}
|
||||
];
|
||||
var errorCode = 1;
|
||||
var notification = {};
|
||||
var apnDevice = {
|
||||
connIndex: 2,
|
||||
appIdentifier: 'bundleId1'
|
||||
};
|
||||
|
||||
APNS.handleTransmissionError(conns, errorCode, notification, apnDevice);
|
||||
|
||||
expect(conns[0].pushNotification).not.toHaveBeenCalled();
|
||||
expect(conns[1].pushNotification).not.toHaveBeenCalled();
|
||||
expect(conns[2].pushNotification).not.toHaveBeenCalled();
|
||||
expect(conns[3].pushNotification).not.toHaveBeenCalled();
|
||||
expect(conns[4].pushNotification).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can handle transmission error when device has no appIdentifier', (done) => {
|
||||
// Mock conns
|
||||
var conns = [
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId1'
|
||||
},
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId2'
|
||||
},
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId3'
|
||||
},
|
||||
];
|
||||
var errorCode = 1;
|
||||
var notification = {};
|
||||
var apnDevice = {
|
||||
connIndex: 1,
|
||||
};
|
||||
|
||||
APNS.handleTransmissionError(conns, errorCode, notification, apnDevice);
|
||||
|
||||
expect(conns[0].pushNotification).not.toHaveBeenCalled();
|
||||
expect(conns[1].pushNotification).not.toHaveBeenCalled();
|
||||
expect(conns[2].pushNotification).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can send APNS notification', (done) => {
|
||||
var args = {
|
||||
cert: 'prodCert.pem',
|
||||
key: 'prodKey.pem',
|
||||
production: true,
|
||||
bundleId: 'bundleId'
|
||||
}
|
||||
var apns = new APNS(args);
|
||||
var conn = {
|
||||
pushNotification: jasmine.createSpy('send'),
|
||||
bundleId: 'bundleId'
|
||||
};
|
||||
apns.conns = [ conn ];
|
||||
// Mock data
|
||||
var expirationTime = 1454571491354
|
||||
var data = {
|
||||
'expiration_time': expirationTime,
|
||||
'data': {
|
||||
'alert': 'alert'
|
||||
}
|
||||
}
|
||||
// Mock devices
|
||||
var devices = [
|
||||
{
|
||||
deviceToken: '112233',
|
||||
appIdentifier: 'bundleId'
|
||||
}
|
||||
];
|
||||
|
||||
var promise = apns.send(data, devices);
|
||||
expect(conn.pushNotification).toHaveBeenCalled();
|
||||
var args = conn.pushNotification.calls.first().args;
|
||||
var notification = args[0];
|
||||
expect(notification.alert).toEqual(data.data.alert);
|
||||
expect(notification.expiry).toEqual(data['expiration_time']);
|
||||
var apnDevice = args[1]
|
||||
expect(apnDevice.connIndex).toEqual(0);
|
||||
expect(apnDevice.appIdentifier).toEqual('bundleId');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
|
||||
var AdaptableController = require("../src/Controllers/AdaptableController").AdaptableController;
|
||||
var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default;
|
||||
var FilesController = require("../src/Controllers/FilesController").FilesController;
|
||||
|
||||
var MockController = function(options) {
|
||||
AdaptableController.call(this, options);
|
||||
}
|
||||
MockController.prototype = Object.create(AdaptableController.prototype);
|
||||
MockController.prototype.constructor = AdaptableController;
|
||||
|
||||
describe("AdaptableController", ()=>{
|
||||
|
||||
it("should use the provided adapter", (done) => {
|
||||
var adapter = new FilesAdapter();
|
||||
var controller = new FilesController(adapter);
|
||||
expect(controller.adapter).toBe(adapter);
|
||||
// make sure _adapter is private
|
||||
expect(controller._adapter).toBe(undefined);
|
||||
// Override _adapter is not doing anything
|
||||
controller._adapter = "Hello";
|
||||
expect(controller.adapter).toBe(adapter);
|
||||
done();
|
||||
});
|
||||
|
||||
it("should throw when creating a new mock controller", (done) => {
|
||||
var adapter = new FilesAdapter();
|
||||
expect(() => {
|
||||
new MockController(adapter);
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it("should fail setting the wrong adapter to the controller", (done) => {
|
||||
function WrongAdapter() {};
|
||||
var adapter = new FilesAdapter();
|
||||
var controller = new FilesController(adapter);
|
||||
var otherAdapter = new WrongAdapter();
|
||||
expect(() => {
|
||||
controller.adapter = otherAdapter;
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it("should fail to instantiate a controller with wrong adapter", (done) => {
|
||||
function WrongAdapter() {};
|
||||
var adapter = new WrongAdapter();
|
||||
expect(() => {
|
||||
new FilesController(adapter);
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it("should fail to instantiate a controller without an adapter", (done) => {
|
||||
expect(() => {
|
||||
new FilesController();
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it("should accept an object adapter", (done) => {
|
||||
var adapter = {
|
||||
createFile: function(config, filename, data) { },
|
||||
deleteFile: function(config, filename) { },
|
||||
getFileData: function(config, filename) { },
|
||||
getFileLocation: function(config, filename) { },
|
||||
}
|
||||
expect(() => {
|
||||
new FilesController(adapter);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it("should accept an object adapter", (done) => {
|
||||
function AGoodAdapter() {};
|
||||
AGoodAdapter.prototype.createFile = function(config, filename, data) { };
|
||||
AGoodAdapter.prototype.deleteFile = function(config, filename) { };
|
||||
AGoodAdapter.prototype.getFileData = function(config, filename) { };
|
||||
AGoodAdapter.prototype.getFileLocation = function(config, filename) { };
|
||||
|
||||
var adapter = new AGoodAdapter();
|
||||
expect(() => {
|
||||
new FilesController(adapter);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
|
||||
var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter;
|
||||
var FilesAdapter = require("parse-server-fs-adapter").default;
|
||||
var S3Adapter = require("parse-server-s3-adapter").default;
|
||||
var GCSAdapter = require("parse-server-gcs-adapter").default;
|
||||
var ParsePushAdapter = require("parse-server-push-adapter").default;
|
||||
|
||||
describe("AdapterLoader", ()=>{
|
||||
|
||||
it("should instantiate an adapter from string in object", (done) => {
|
||||
var adapterPath = require('path').resolve("./spec/MockAdapter");
|
||||
|
||||
var adapter = loadAdapter({
|
||||
adapter: adapterPath,
|
||||
options: {
|
||||
key: "value",
|
||||
foo: "bar"
|
||||
}
|
||||
});
|
||||
|
||||
expect(adapter instanceof Object).toBe(true);
|
||||
expect(adapter.options.key).toBe("value");
|
||||
expect(adapter.options.foo).toBe("bar");
|
||||
done();
|
||||
});
|
||||
|
||||
it("should instantiate an adapter from string", (done) => {
|
||||
var adapterPath = require('path').resolve("./spec/MockAdapter");
|
||||
var adapter = loadAdapter(adapterPath);
|
||||
|
||||
expect(adapter instanceof Object).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
it("should instantiate an adapter from string that is module", (done) => {
|
||||
var adapterPath = require('path').resolve("./src/Adapters/Files/FilesAdapter");
|
||||
var adapter = loadAdapter({
|
||||
adapter: adapterPath
|
||||
});
|
||||
|
||||
expect(typeof adapter).toBe('object');
|
||||
expect(typeof adapter.createFile).toBe('function');
|
||||
expect(typeof adapter.deleteFile).toBe('function');
|
||||
expect(typeof adapter.getFileData).toBe('function');
|
||||
expect(typeof adapter.getFileLocation).toBe('function');
|
||||
done();
|
||||
});
|
||||
|
||||
it("should instantiate an adapter from function/Class", (done) => {
|
||||
var adapter = loadAdapter({
|
||||
adapter: FilesAdapter
|
||||
});
|
||||
expect(adapter instanceof FilesAdapter).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
it("should instantiate the default adapter from Class", (done) => {
|
||||
var adapter = loadAdapter(null, FilesAdapter);
|
||||
expect(adapter instanceof FilesAdapter).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
it("should use the default adapter", (done) => {
|
||||
var defaultAdapter = new FilesAdapter();
|
||||
var adapter = loadAdapter(null, defaultAdapter);
|
||||
expect(adapter instanceof FilesAdapter).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
it("should use the provided adapter", (done) => {
|
||||
var originalAdapter = new FilesAdapter();
|
||||
var adapter = loadAdapter(originalAdapter);
|
||||
expect(adapter).toBe(originalAdapter);
|
||||
done();
|
||||
});
|
||||
|
||||
it("should fail loading an improperly configured adapter", (done) => {
|
||||
var Adapter = function(options) {
|
||||
if (!options.foo) {
|
||||
throw "foo is required for that adapter";
|
||||
}
|
||||
}
|
||||
var adapterOptions = {
|
||||
param: "key",
|
||||
doSomething: function() {}
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
var adapter = loadAdapter(adapterOptions, Adapter);
|
||||
expect(adapter).toEqual(adapterOptions);
|
||||
}).not.toThrow("foo is required for that adapter");
|
||||
done();
|
||||
});
|
||||
|
||||
it("should load push adapter from options", (done) => {
|
||||
var options = {
|
||||
ios: {
|
||||
bundleId: 'bundle.id'
|
||||
}
|
||||
}
|
||||
expect(() => {
|
||||
var adapter = loadAdapter(undefined, ParsePushAdapter, options);
|
||||
expect(adapter.constructor).toBe(ParsePushAdapter);
|
||||
expect(adapter).not.toBe(undefined);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it("should load S3Adapter from direct passing", (done) => {
|
||||
var s3Adapter = new S3Adapter("key", "secret", "bucket")
|
||||
expect(() => {
|
||||
var adapter = loadAdapter(s3Adapter, FilesAdapter);
|
||||
expect(adapter).toBe(s3Adapter);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
})
|
||||
|
||||
it("should load GCSAdapter from direct passing", (done) => {
|
||||
var gcsAdapter = new GCSAdapter("projectId", "path/to/keyfile", "bucket")
|
||||
expect(() => {
|
||||
var adapter = loadAdapter(gcsAdapter, FilesAdapter);
|
||||
expect(adapter).toBe(gcsAdapter);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
'use strict';
|
||||
var commander = require("../src/cli/utils/commander").default;
|
||||
|
||||
var definitions = {
|
||||
"arg0": "PROGRAM_ARG_0",
|
||||
"arg1": {
|
||||
env: "PROGRAM_ARG_1",
|
||||
required: true
|
||||
},
|
||||
"arg2": {
|
||||
env: "PROGRAM_ARG_2",
|
||||
action: function(value) {
|
||||
var value = parseInt(value);
|
||||
if (!Number.isInteger(value)) {
|
||||
throw "arg2 is invalid";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
},
|
||||
"arg3": {},
|
||||
"arg4": {
|
||||
default: "arg4Value"
|
||||
}
|
||||
}
|
||||
|
||||
describe("commander additions", () => {
|
||||
|
||||
afterEach((done) => {
|
||||
commander.options = [];
|
||||
delete commander.arg0;
|
||||
delete commander.arg1;
|
||||
delete commander.arg2;
|
||||
delete commander.arg3;
|
||||
delete commander.arg4;
|
||||
done();
|
||||
})
|
||||
|
||||
it("should load properly definitions from args", (done) => {
|
||||
commander.loadDefinitions(definitions);
|
||||
commander.parse(["node","./CLI.spec.js","--arg0", "arg0Value", "--arg1", "arg1Value", "--arg2", "2", "--arg3", "some"]);
|
||||
expect(commander.arg0).toEqual("arg0Value");
|
||||
expect(commander.arg1).toEqual("arg1Value");
|
||||
expect(commander.arg2).toEqual(2);
|
||||
expect(commander.arg3).toEqual("some");
|
||||
expect(commander.arg4).toEqual("arg4Value");
|
||||
done();
|
||||
});
|
||||
|
||||
it("should load properly definitions from env", (done) => {
|
||||
commander.loadDefinitions(definitions);
|
||||
commander.parse([], {
|
||||
"PROGRAM_ARG_0": "arg0ENVValue",
|
||||
"PROGRAM_ARG_1": "arg1ENVValue",
|
||||
"PROGRAM_ARG_2": "3",
|
||||
});
|
||||
expect(commander.arg0).toEqual("arg0ENVValue");
|
||||
expect(commander.arg1).toEqual("arg1ENVValue");
|
||||
expect(commander.arg2).toEqual(3);
|
||||
expect(commander.arg4).toEqual("arg4Value");
|
||||
done();
|
||||
});
|
||||
|
||||
it("should load properly use args over env", (done) => {
|
||||
commander.loadDefinitions(definitions);
|
||||
commander.parse(["node","./CLI.spec.js","--arg0", "arg0Value", "--arg4", "anotherArg4"], {
|
||||
"PROGRAM_ARG_0": "arg0ENVValue",
|
||||
"PROGRAM_ARG_1": "arg1ENVValue",
|
||||
"PROGRAM_ARG_2": "4",
|
||||
});
|
||||
expect(commander.arg0).toEqual("arg0Value");
|
||||
expect(commander.arg1).toEqual("arg1ENVValue");
|
||||
expect(commander.arg2).toEqual(4);
|
||||
expect(commander.arg4).toEqual("anotherArg4");
|
||||
done();
|
||||
});
|
||||
|
||||
it("should fail in action as port is invalid", (done) => {
|
||||
commander.loadDefinitions(definitions);
|
||||
expect(()=> {
|
||||
commander.parse(["node","./CLI.spec.js","--arg0", "arg0Value"], {
|
||||
"PROGRAM_ARG_0": "arg0ENVValue",
|
||||
"PROGRAM_ARG_1": "arg1ENVValue",
|
||||
"PROGRAM_ARG_2": "hello",
|
||||
});
|
||||
}).toThrow("arg2 is invalid");
|
||||
done();
|
||||
});
|
||||
|
||||
it("should not override config.json", (done) => {
|
||||
commander.loadDefinitions(definitions);
|
||||
commander.parse(["node","./CLI.spec.js","--arg0", "arg0Value", "./spec/configs/CLIConfig.json"], {
|
||||
"PROGRAM_ARG_0": "arg0ENVValue",
|
||||
"PROGRAM_ARG_1": "arg1ENVValue",
|
||||
});
|
||||
let options = commander.getOptions();
|
||||
expect(options.arg2).toBe(8888);
|
||||
expect(options.arg3).toBe("hello"); //config value
|
||||
expect(options.arg4).toBe('/1');
|
||||
done();
|
||||
});
|
||||
|
||||
it("should fail with invalid values in JSON", (done) => {
|
||||
commander.loadDefinitions(definitions);
|
||||
expect(() => {
|
||||
commander.parse(["node","./CLI.spec.js","--arg0", "arg0Value", "./spec/configs/CLIConfigFail.json"], {
|
||||
"PROGRAM_ARG_0": "arg0ENVValue",
|
||||
"PROGRAM_ARG_1": "arg1ENVValue",
|
||||
});
|
||||
}).toThrow("arg2 is invalid")
|
||||
done();
|
||||
});
|
||||
|
||||
it("should fail when too many apps are set", (done) => {
|
||||
commander.loadDefinitions(definitions);
|
||||
expect(() => {
|
||||
commander.parse(["node","./CLI.spec.js","./spec/configs/CLIConfigFailTooManyApps.json"]);
|
||||
}).toThrow("Multiple apps are not supported")
|
||||
done();
|
||||
});
|
||||
|
||||
it("should load config from apps", (done) => {
|
||||
commander.loadDefinitions(definitions);
|
||||
commander.parse(["node", "./CLI.spec.js", "./spec/configs/CLIConfigApps.json"]);
|
||||
let options = commander.getOptions();
|
||||
expect(options.arg1).toBe("my_app");
|
||||
expect(options.arg2).toBe(8888);
|
||||
expect(options.arg3).toBe("hello"); //config value
|
||||
expect(options.arg4).toBe('/1');
|
||||
done();
|
||||
});
|
||||
|
||||
it("should fail when passing an invalid arguement", (done) => {
|
||||
commander.loadDefinitions(definitions);
|
||||
expect(() => {
|
||||
commander.parse(["node", "./CLI.spec.js", "./spec/configs/CLIConfigUnknownArg.json"]);
|
||||
}).toThrow('error: unknown option myArg')
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,290 @@
|
||||
var Client = require('../src/LiveQuery/Client').Client;
|
||||
var ParseWebSocket = require('../src/LiveQuery/ParseWebSocketServer').ParseWebSocket;
|
||||
|
||||
describe('Client', function() {
|
||||
|
||||
it('can be initialized', function() {
|
||||
var parseWebSocket = new ParseWebSocket({});
|
||||
var client = new Client(1, parseWebSocket);
|
||||
|
||||
expect(client.id).toBe(1);
|
||||
expect(client.parseWebSocket).toBe(parseWebSocket);
|
||||
expect(client.subscriptionInfos.size).toBe(0);
|
||||
});
|
||||
|
||||
it('can push response', function() {
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
Client.pushResponse(parseWebSocket, 'message');
|
||||
|
||||
expect(parseWebSocket.send).toHaveBeenCalledWith('message');
|
||||
});
|
||||
|
||||
it('can push error', function() {
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
Client.pushError(parseWebSocket, 1, 'error', true);
|
||||
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('error');
|
||||
expect(messageJSON.error).toBe('error');
|
||||
expect(messageJSON.code).toBe(1);
|
||||
expect(messageJSON.reconnect).toBe(true);
|
||||
});
|
||||
|
||||
it('can add subscription information', function() {
|
||||
var subscription = {};
|
||||
var fields = ['test'];
|
||||
var subscriptionInfo = {
|
||||
subscription: subscription,
|
||||
fields: fields
|
||||
}
|
||||
var client = new Client(1, {});
|
||||
client.addSubscriptionInfo(1, subscriptionInfo);
|
||||
|
||||
expect(client.subscriptionInfos.size).toBe(1);
|
||||
expect(client.subscriptionInfos.get(1)).toBe(subscriptionInfo);
|
||||
});
|
||||
|
||||
it('can get subscription information', function() {
|
||||
var subscription = {};
|
||||
var fields = ['test'];
|
||||
var subscriptionInfo = {
|
||||
subscription: subscription,
|
||||
fields: fields
|
||||
}
|
||||
var client = new Client(1, {});
|
||||
client.addSubscriptionInfo(1, subscriptionInfo);
|
||||
var subscriptionInfoAgain = client.getSubscriptionInfo(1);
|
||||
|
||||
expect(subscriptionInfoAgain).toBe(subscriptionInfo);
|
||||
});
|
||||
|
||||
it('can delete subscription information', function() {
|
||||
var subscription = {};
|
||||
var fields = ['test'];
|
||||
var subscriptionInfo = {
|
||||
subscription: subscription,
|
||||
fields: fields
|
||||
}
|
||||
var client = new Client(1, {});
|
||||
client.addSubscriptionInfo(1, subscriptionInfo);
|
||||
client.deleteSubscriptionInfo(1);
|
||||
|
||||
expect(client.subscriptionInfos.size).toBe(0);
|
||||
});
|
||||
|
||||
|
||||
it('can generate ParseObject JSON with null selected field', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
};
|
||||
var client = new Client(1, {});
|
||||
|
||||
expect(client._toJSONWithFields(parseObjectJSON, null)).toBe(parseObjectJSON);
|
||||
});
|
||||
|
||||
it('can generate ParseObject JSON with undefined selected field', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
};
|
||||
var client = new Client(1, {});
|
||||
|
||||
expect(client._toJSONWithFields(parseObjectJSON, undefined)).toBe(parseObjectJSON);
|
||||
});
|
||||
|
||||
it('can generate ParseObject JSON with selected fields', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
test: 'test'
|
||||
};
|
||||
var client = new Client(1, {});
|
||||
|
||||
expect(client._toJSONWithFields(parseObjectJSON, ['test'])).toEqual({
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
test: 'test'
|
||||
});
|
||||
});
|
||||
|
||||
it('can generate ParseObject JSON with nonexistent selected fields', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
test: 'test'
|
||||
};
|
||||
var client = new Client(1, {});
|
||||
var limitedParseObject = client._toJSONWithFields(parseObjectJSON, ['name']);
|
||||
|
||||
expect(limitedParseObject).toEqual({
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
});
|
||||
expect('name' in limitedParseObject).toBe(false);
|
||||
});
|
||||
|
||||
it('can push connect response', function() {
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
var client = new Client(1, parseWebSocket);
|
||||
client.pushConnect();
|
||||
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('connected');
|
||||
expect(messageJSON.clientId).toBe(1);
|
||||
});
|
||||
|
||||
it('can push subscribe response', function() {
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
var client = new Client(1, parseWebSocket);
|
||||
client.pushSubscribe(2);
|
||||
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('subscribed');
|
||||
expect(messageJSON.clientId).toBe(1);
|
||||
expect(messageJSON.requestId).toBe(2);
|
||||
});
|
||||
|
||||
it('can push unsubscribe response', function() {
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
var client = new Client(1, parseWebSocket);
|
||||
client.pushUnsubscribe(2);
|
||||
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('unsubscribed');
|
||||
expect(messageJSON.clientId).toBe(1);
|
||||
expect(messageJSON.requestId).toBe(2);
|
||||
});
|
||||
|
||||
it('can push create response', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
test: 'test'
|
||||
};
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
var client = new Client(1, parseWebSocket);
|
||||
client.pushCreate(2, parseObjectJSON);
|
||||
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('create');
|
||||
expect(messageJSON.clientId).toBe(1);
|
||||
expect(messageJSON.requestId).toBe(2);
|
||||
expect(messageJSON.object).toEqual(parseObjectJSON);
|
||||
});
|
||||
|
||||
it('can push enter response', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
test: 'test'
|
||||
};
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
var client = new Client(1, parseWebSocket);
|
||||
client.pushEnter(2, parseObjectJSON);
|
||||
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('enter');
|
||||
expect(messageJSON.clientId).toBe(1);
|
||||
expect(messageJSON.requestId).toBe(2);
|
||||
expect(messageJSON.object).toEqual(parseObjectJSON);
|
||||
});
|
||||
|
||||
it('can push update response', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
test: 'test'
|
||||
};
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
var client = new Client(1, parseWebSocket);
|
||||
client.pushUpdate(2, parseObjectJSON);
|
||||
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('update');
|
||||
expect(messageJSON.clientId).toBe(1);
|
||||
expect(messageJSON.requestId).toBe(2);
|
||||
expect(messageJSON.object).toEqual(parseObjectJSON);
|
||||
});
|
||||
|
||||
it('can push leave response', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
test: 'test'
|
||||
};
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
var client = new Client(1, parseWebSocket);
|
||||
client.pushLeave(2, parseObjectJSON);
|
||||
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('leave');
|
||||
expect(messageJSON.clientId).toBe(1);
|
||||
expect(messageJSON.requestId).toBe(2);
|
||||
expect(messageJSON.object).toEqual(parseObjectJSON);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
let DatabaseAdapter = require('../src/DatabaseAdapter');
|
||||
|
||||
describe('DatabaseAdapter', () => {
|
||||
it('options and URI are available to adapter', done => {
|
||||
DatabaseAdapter.setAppDatabaseURI('optionsTest', 'mongodb://localhost:27017/optionsTest');
|
||||
DatabaseAdapter.setAppDatabaseOptions('optionsTest', {foo: "bar"});
|
||||
let optionsTestDatabaseConnection = DatabaseAdapter.getDatabaseConnection('optionsTest');
|
||||
|
||||
expect(optionsTestDatabaseConnection instanceof Object).toBe(true);
|
||||
expect(optionsTestDatabaseConnection.adapter._options instanceof Object).toBe(true);
|
||||
expect(optionsTestDatabaseConnection.adapter._options.foo).toBe("bar");
|
||||
|
||||
DatabaseAdapter.setAppDatabaseURI('noOptionsTest', 'mongodb://localhost:27017/noOptionsTest');
|
||||
let noOptionsTestDatabaseConnection = DatabaseAdapter.getDatabaseConnection('noOptionsTest');
|
||||
|
||||
expect(noOptionsTestDatabaseConnection instanceof Object).toBe(true);
|
||||
expect(noOptionsTestDatabaseConnection.adapter._options instanceof Object).toBe(false);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
let DatabaseController = require('../src/Controllers/DatabaseController');
|
||||
let MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
|
||||
|
||||
describe('DatabaseController', () => {
|
||||
it('can be constructed', done => {
|
||||
let adapter = new MongoStorageAdapter('mongodb://localhost:27017/test');
|
||||
let databaseController = new DatabaseController(adapter, {
|
||||
collectionPrefix: 'test_'
|
||||
});
|
||||
databaseController.connect().then(done, error => {
|
||||
console.log('error', error.stack);
|
||||
fail();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
|
||||
|
||||
describe('EventEmitterPubSub', function() {
|
||||
|
||||
it('can publish and subscribe', function() {
|
||||
var publisher = EventEmitterPubSub.createPublisher();
|
||||
var subscriber = EventEmitterPubSub.createSubscriber();
|
||||
subscriber.subscribe('testChannel');
|
||||
// Register mock checked for subscriber
|
||||
var isChecked = false;
|
||||
subscriber.on('message', function(channel, message) {
|
||||
isChecked = true;
|
||||
expect(channel).toBe('testChannel');
|
||||
expect(message).toBe('testMessage');
|
||||
});
|
||||
|
||||
publisher.publish('testChannel', 'testMessage');
|
||||
// Make sure the callback is checked
|
||||
expect(isChecked).toBe(true);
|
||||
});
|
||||
|
||||
it('can unsubscribe', function() {
|
||||
var publisher = EventEmitterPubSub.createPublisher();
|
||||
var subscriber = EventEmitterPubSub.createSubscriber();
|
||||
subscriber.subscribe('testChannel');
|
||||
subscriber.unsubscribe('testChannel');
|
||||
// Register mock checked for subscriber
|
||||
var isCalled = false;
|
||||
subscriber.on('message', function(channel, message) {
|
||||
isCalled = true;
|
||||
});
|
||||
|
||||
publisher.publish('testChannel', 'testMessage');
|
||||
// Make sure the callback is not called
|
||||
expect(isCalled).toBe(false);
|
||||
});
|
||||
|
||||
it('can unsubscribe not subscribing channel', function() {
|
||||
var subscriber = EventEmitterPubSub.createSubscriber();
|
||||
|
||||
// Make sure subscriber does not throw exception
|
||||
subscriber.unsubscribe('testChannel');
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
var ExportAdapter = require('../src/ExportAdapter');
|
||||
|
||||
describe('ExportAdapter', () => {
|
||||
it('can be constructed', (done) => {
|
||||
var database = new ExportAdapter('mongodb://localhost:27017/test',
|
||||
{
|
||||
collectionPrefix: 'test_'
|
||||
});
|
||||
database.connect().then(done, (error) => {
|
||||
console.log('error', error.stack);
|
||||
fail();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,42 +1,22 @@
|
||||
var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter;
|
||||
var Parse = require('parse/node').Parse;
|
||||
var request = require('request');
|
||||
var fs = require('fs');
|
||||
|
||||
var LOGS_FOLDER = './test_logs/';
|
||||
|
||||
var deleteFolderRecursive = function(path) {
|
||||
if( fs.existsSync(path) ) {
|
||||
fs.readdirSync(path).forEach(function(file,index){
|
||||
var curPath = path + "/" + file;
|
||||
if(fs.lstatSync(curPath).isDirectory()) { // recurse
|
||||
deleteFolderRecursive(curPath);
|
||||
} else { // delete file
|
||||
fs.unlinkSync(curPath);
|
||||
}
|
||||
});
|
||||
fs.rmdirSync(path);
|
||||
}
|
||||
};
|
||||
|
||||
describe('info logs', () => {
|
||||
|
||||
afterEach((done) => {
|
||||
deleteFolderRecursive(LOGS_FOLDER);
|
||||
done();
|
||||
});
|
||||
|
||||
it("Verify INFO logs", (done) => {
|
||||
var fileLoggerAdapter = new FileLoggerAdapter({
|
||||
logsFolder: LOGS_FOLDER
|
||||
});
|
||||
var fileLoggerAdapter = new FileLoggerAdapter();
|
||||
fileLoggerAdapter.info('testing info logs', () => {
|
||||
fileLoggerAdapter.query({
|
||||
size: 1,
|
||||
level: 'info'
|
||||
}, (results) => {
|
||||
expect(results[0].message).toEqual('testing info logs');
|
||||
done();
|
||||
if(results.length == 0) {
|
||||
fail('The adapter should return non-empty results');
|
||||
done();
|
||||
} else {
|
||||
expect(results[0].message).toEqual('testing info logs');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -44,11 +24,6 @@ describe('info logs', () => {
|
||||
|
||||
describe('error logs', () => {
|
||||
|
||||
afterEach((done) => {
|
||||
deleteFolderRecursive(LOGS_FOLDER);
|
||||
done();
|
||||
});
|
||||
|
||||
it("Verify ERROR logs", (done) => {
|
||||
var fileLoggerAdapter = new FileLoggerAdapter();
|
||||
fileLoggerAdapter.error('testing error logs', () => {
|
||||
@@ -56,8 +31,14 @@ describe('error logs', () => {
|
||||
size: 1,
|
||||
level: 'error'
|
||||
}, (results) => {
|
||||
expect(results[0].message).toEqual('testing error logs');
|
||||
done();
|
||||
if(results.length == 0) {
|
||||
fail('The adapter should return non-empty results');
|
||||
done();
|
||||
}
|
||||
else {
|
||||
expect(results[0].message).toEqual('testing error logs');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
var GridStoreAdapter = require("../src/Adapters/Files/GridStoreAdapter").GridStoreAdapter;
|
||||
var Config = require("../src/Config");
|
||||
var FilesController = require('../src/Controllers/FilesController').default;
|
||||
|
||||
|
||||
// Small additional tests to improve overall coverage
|
||||
describe("FilesController",() =>{
|
||||
it("should properly expand objects", (done) => {
|
||||
|
||||
var config = new Config(Parse.applicationId);
|
||||
var gridStoreAdapter = new GridStoreAdapter('mongodb://localhost:27017/parse');
|
||||
var filesController = new FilesController(gridStoreAdapter)
|
||||
var result = filesController.expandFilesInObject(config, function(){});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
|
||||
var fullFile = {
|
||||
type: '__type',
|
||||
url: "http://an.url"
|
||||
}
|
||||
|
||||
var anObject = {
|
||||
aFile: fullFile
|
||||
}
|
||||
filesController.expandFilesInObject(config, anObject);
|
||||
expect(anObject.aFile.url).toEqual("http://an.url");
|
||||
|
||||
done();
|
||||
})
|
||||
});
|
||||
@@ -1,159 +0,0 @@
|
||||
var GCM = require('../src/GCM');
|
||||
|
||||
describe('GCM', () => {
|
||||
it('can initialize', (done) => {
|
||||
var args = {
|
||||
apiKey: 'apiKey'
|
||||
};
|
||||
var gcm = new GCM(args);
|
||||
expect(gcm.sender.key).toBe(args.apiKey);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can throw on initializing with invalid args', (done) => {
|
||||
var args = 123
|
||||
expect(function() {
|
||||
new GCM(args);
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can generate GCM Payload without expiration time', (done) => {
|
||||
//Mock request data
|
||||
var data = {
|
||||
'alert': 'alert'
|
||||
};
|
||||
var pushId = 1;
|
||||
var timeStamp = 1454538822113;
|
||||
var timeStampISOStr = new Date(timeStamp).toISOString();
|
||||
|
||||
var payload = GCM.generateGCMPayload(data, pushId, timeStamp);
|
||||
|
||||
expect(payload.priority).toEqual('normal');
|
||||
expect(payload.timeToLive).toEqual(undefined);
|
||||
var dataFromPayload = payload.data;
|
||||
expect(dataFromPayload.time).toEqual(timeStampISOStr);
|
||||
expect(dataFromPayload['push_id']).toEqual(pushId);
|
||||
var dataFromUser = JSON.parse(dataFromPayload.data);
|
||||
expect(dataFromUser).toEqual(data);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can generate GCM Payload with valid expiration time', (done) => {
|
||||
//Mock request data
|
||||
var data = {
|
||||
'alert': 'alert'
|
||||
};
|
||||
var pushId = 1;
|
||||
var timeStamp = 1454538822113;
|
||||
var timeStampISOStr = new Date(timeStamp).toISOString();
|
||||
var expirationTime = 1454538922113
|
||||
|
||||
var payload = GCM.generateGCMPayload(data, pushId, timeStamp, expirationTime);
|
||||
|
||||
expect(payload.priority).toEqual('normal');
|
||||
expect(payload.timeToLive).toEqual(Math.floor((expirationTime - timeStamp) / 1000));
|
||||
var dataFromPayload = payload.data;
|
||||
expect(dataFromPayload.time).toEqual(timeStampISOStr);
|
||||
expect(dataFromPayload['push_id']).toEqual(pushId);
|
||||
var dataFromUser = JSON.parse(dataFromPayload.data);
|
||||
expect(dataFromUser).toEqual(data);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can generate GCM Payload with too early expiration time', (done) => {
|
||||
//Mock request data
|
||||
var data = {
|
||||
'alert': 'alert'
|
||||
};
|
||||
var pushId = 1;
|
||||
var timeStamp = 1454538822113;
|
||||
var timeStampISOStr = new Date(timeStamp).toISOString();
|
||||
var expirationTime = 1454538822112;
|
||||
|
||||
var payload = GCM.generateGCMPayload(data, pushId, timeStamp, expirationTime);
|
||||
|
||||
expect(payload.priority).toEqual('normal');
|
||||
expect(payload.timeToLive).toEqual(0);
|
||||
var dataFromPayload = payload.data;
|
||||
expect(dataFromPayload.time).toEqual(timeStampISOStr);
|
||||
expect(dataFromPayload['push_id']).toEqual(pushId);
|
||||
var dataFromUser = JSON.parse(dataFromPayload.data);
|
||||
expect(dataFromUser).toEqual(data);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can generate GCM Payload with too late expiration time', (done) => {
|
||||
//Mock request data
|
||||
var data = {
|
||||
'alert': 'alert'
|
||||
};
|
||||
var pushId = 1;
|
||||
var timeStamp = 1454538822113;
|
||||
var timeStampISOStr = new Date(timeStamp).toISOString();
|
||||
var expirationTime = 2454538822113;
|
||||
|
||||
var payload = GCM.generateGCMPayload(data, pushId, timeStamp, expirationTime);
|
||||
|
||||
expect(payload.priority).toEqual('normal');
|
||||
// Four week in second
|
||||
expect(payload.timeToLive).toEqual(4 * 7 * 24 * 60 * 60);
|
||||
var dataFromPayload = payload.data;
|
||||
expect(dataFromPayload.time).toEqual(timeStampISOStr);
|
||||
expect(dataFromPayload['push_id']).toEqual(pushId);
|
||||
var dataFromUser = JSON.parse(dataFromPayload.data);
|
||||
expect(dataFromUser).toEqual(data);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can send GCM request', (done) => {
|
||||
var gcm = new GCM({
|
||||
apiKey: 'apiKey'
|
||||
});
|
||||
// Mock gcm sender
|
||||
var sender = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
gcm.sender = sender;
|
||||
// Mock data
|
||||
var expirationTime = 2454538822113;
|
||||
var data = {
|
||||
'expiration_time': expirationTime,
|
||||
'data': {
|
||||
'alert': 'alert'
|
||||
}
|
||||
}
|
||||
// Mock devices
|
||||
var devices = [
|
||||
{
|
||||
deviceToken: 'token'
|
||||
}
|
||||
];
|
||||
|
||||
gcm.send(data, devices);
|
||||
expect(sender.send).toHaveBeenCalled();
|
||||
var args = sender.send.calls.first().args;
|
||||
// It is too hard to verify message of gcm library, we just verify tokens and retry times
|
||||
expect(args[1].registrationTokens).toEqual(['token']);
|
||||
expect(args[2]).toEqual(5);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can slice devices', (done) => {
|
||||
// Mock devices
|
||||
var devices = [makeDevice(1), makeDevice(2), makeDevice(3), makeDevice(4)];
|
||||
|
||||
var chunkDevices = GCM.sliceDevices(devices, 3);
|
||||
expect(chunkDevices).toEqual([
|
||||
[makeDevice(1), makeDevice(2), makeDevice(3)],
|
||||
[makeDevice(4)]
|
||||
]);
|
||||
done();
|
||||
});
|
||||
|
||||
function makeDevice(deviceToken) {
|
||||
return {
|
||||
deviceToken: deviceToken
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,260 @@
|
||||
'use strict';
|
||||
|
||||
var httpRequest = require("../src/cloud-code/httpRequest"),
|
||||
bodyParser = require('body-parser'),
|
||||
express = require("express");
|
||||
|
||||
var port = 13371;
|
||||
var httpRequestServer = "http://localhost:"+port;
|
||||
|
||||
var app = express();
|
||||
app.use(bodyParser.json({ 'type': '*/*' }));
|
||||
app.get("/hello", function(req, res){
|
||||
res.json({response: "OK"});
|
||||
});
|
||||
|
||||
app.get("/404", function(req, res){
|
||||
res.status(404);
|
||||
res.send("NO");
|
||||
});
|
||||
|
||||
app.get("/301", function(req, res){
|
||||
res.status(301);
|
||||
res.location("/hello");
|
||||
res.send();
|
||||
});
|
||||
|
||||
app.post('/echo', function(req, res){
|
||||
res.json(req.body);
|
||||
});
|
||||
|
||||
app.get('/qs', function(req, res){
|
||||
res.json(req.query);
|
||||
});
|
||||
|
||||
app.listen(13371);
|
||||
|
||||
|
||||
describe("httpRequest", () => {
|
||||
|
||||
it("should do /hello", (done) => {
|
||||
httpRequest({
|
||||
url: httpRequestServer+"/hello"
|
||||
}).then(function(httpResponse){
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}'));
|
||||
expect(httpResponse.text).toEqual('{"response":"OK"}');
|
||||
expect(httpResponse.data.response).toEqual("OK");
|
||||
done();
|
||||
}, function(){
|
||||
fail("should not fail");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should do /hello with callback and promises", (done) => {
|
||||
var calls = 0;
|
||||
httpRequest({
|
||||
url: httpRequestServer+"/hello",
|
||||
success: function() { calls++; },
|
||||
error: function() { calls++; }
|
||||
}).then(function(httpResponse){
|
||||
expect(calls).toBe(1);
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}'));
|
||||
expect(httpResponse.text).toEqual('{"response":"OK"}');
|
||||
expect(httpResponse.data.response).toEqual("OK");
|
||||
done();
|
||||
}, function(){
|
||||
fail("should not fail");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should do not follow redirects by default", (done) => {
|
||||
|
||||
httpRequest({
|
||||
url: httpRequestServer+"/301"
|
||||
}).then(function(httpResponse){
|
||||
expect(httpResponse.status).toBe(301);
|
||||
done();
|
||||
}, function(){
|
||||
fail("should not fail");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should follow redirects when set", (done) => {
|
||||
|
||||
httpRequest({
|
||||
url: httpRequestServer+"/301",
|
||||
followRedirects: true
|
||||
}).then(function(httpResponse){
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}'));
|
||||
expect(httpResponse.text).toEqual('{"response":"OK"}');
|
||||
expect(httpResponse.data.response).toEqual("OK");
|
||||
done();
|
||||
}, function(){
|
||||
fail("should not fail");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should fail on 404", (done) => {
|
||||
var calls = 0;
|
||||
httpRequest({
|
||||
url: httpRequestServer+"/404",
|
||||
success: function() {
|
||||
calls++;
|
||||
fail("should not succeed");
|
||||
done();
|
||||
},
|
||||
error: function(httpResponse) {
|
||||
calls++;
|
||||
expect(calls).toBe(1);
|
||||
expect(httpResponse.status).toBe(404);
|
||||
expect(httpResponse.buffer).toEqual(new Buffer('NO'));
|
||||
expect(httpResponse.text).toEqual('NO');
|
||||
expect(httpResponse.data).toBe(undefined);
|
||||
done();
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
it("should fail on 404", (done) => {
|
||||
httpRequest({
|
||||
url: httpRequestServer+"/404",
|
||||
}).then(function(httpResponse){
|
||||
fail("should not succeed");
|
||||
done();
|
||||
}, function(httpResponse){
|
||||
expect(httpResponse.status).toBe(404);
|
||||
expect(httpResponse.buffer).toEqual(new Buffer('NO'));
|
||||
expect(httpResponse.text).toEqual('NO');
|
||||
expect(httpResponse.data).toBe(undefined);
|
||||
done();
|
||||
})
|
||||
})
|
||||
|
||||
it("should post on echo", (done) => {
|
||||
var calls = 0;
|
||||
httpRequest({
|
||||
method: "POST",
|
||||
url: httpRequestServer+"/echo",
|
||||
body: {
|
||||
foo: "bar"
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
success: function() { calls++; },
|
||||
error: function() { calls++; }
|
||||
}).then(function(httpResponse){
|
||||
expect(calls).toBe(1);
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.data).toEqual({foo: "bar"});
|
||||
done();
|
||||
}, function(httpResponse){
|
||||
fail("should not fail");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should encode a query string body by default", (done) => {
|
||||
let options = {
|
||||
body: {"foo": "bar"},
|
||||
}
|
||||
let result = httpRequest.encodeBody(options);
|
||||
expect(result.body).toEqual('foo=bar');
|
||||
expect(result.headers['Content-Type']).toEqual('application/x-www-form-urlencoded');
|
||||
done();
|
||||
|
||||
})
|
||||
|
||||
it("should encode a JSON body", (done) => {
|
||||
let options = {
|
||||
body: {"foo": "bar"},
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
}
|
||||
let result = httpRequest.encodeBody(options);
|
||||
expect(result.body).toEqual('{"foo":"bar"}');
|
||||
done();
|
||||
|
||||
})
|
||||
it("should encode a www-form body", (done) => {
|
||||
let options = {
|
||||
body: {"foo": "bar", "bar": "baz"},
|
||||
headers: {'cOntent-tYpe': 'application/x-www-form-urlencoded'}
|
||||
}
|
||||
let result = httpRequest.encodeBody(options);
|
||||
expect(result.body).toEqual("foo=bar&bar=baz");
|
||||
done();
|
||||
});
|
||||
it("should not encode a wrong content type", (done) => {
|
||||
let options = {
|
||||
body:{"foo": "bar", "bar": "baz"},
|
||||
headers: {'cOntent-tYpe': 'mime/jpeg'}
|
||||
}
|
||||
let result = httpRequest.encodeBody(options);
|
||||
expect(result.body).toEqual({"foo": "bar", "bar": "baz"});
|
||||
done();
|
||||
});
|
||||
|
||||
it("should fail gracefully", (done) => {
|
||||
httpRequest({
|
||||
url: "http://not a good url",
|
||||
success: function() {
|
||||
fail("should not succeed");
|
||||
done();
|
||||
},
|
||||
error: function(error) {
|
||||
expect(error).not.toBeUndefined();
|
||||
expect(error).not.toBeNull();
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should get a cat image', (done) => {
|
||||
httpRequest({
|
||||
url: 'http://thecatapi.com/api/images/get?format=src&type=jpg',
|
||||
followRedirects: true
|
||||
}).then((res) => {
|
||||
expect(res.buffer).not.toBe(null);
|
||||
expect(res.text).not.toBe(null);
|
||||
done();
|
||||
})
|
||||
})
|
||||
|
||||
it("should params object to query string", (done) => {
|
||||
httpRequest({
|
||||
url: httpRequestServer+"/qs",
|
||||
params: {
|
||||
foo: "bar"
|
||||
}
|
||||
}).then(function(httpResponse){
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.data).toEqual({foo: "bar"});
|
||||
done();
|
||||
}, function(){
|
||||
fail("should not fail");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should params string to query string", (done) => {
|
||||
httpRequest({
|
||||
url: httpRequestServer+"/qs",
|
||||
params: "foo=bar&foo2=bar2"
|
||||
}).then(function(httpResponse){
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.data).toEqual({foo: "bar", foo2: 'bar2'});
|
||||
done();
|
||||
}, function(){
|
||||
fail("should not fail");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
@@ -2,53 +2,90 @@ var LoggerController = require('../src/Controllers/LoggerController').LoggerCont
|
||||
var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter;
|
||||
|
||||
describe('LoggerController', () => {
|
||||
it('can check valid master key of request', (done) => {
|
||||
it('can check process a query witout throwing', (done) => {
|
||||
// Make mock request
|
||||
var request = {
|
||||
auth: {
|
||||
isMaster: true
|
||||
},
|
||||
query: {}
|
||||
};
|
||||
var query = {};
|
||||
|
||||
var loggerController = new LoggerController(new FileLoggerAdapter());
|
||||
|
||||
expect(() => {
|
||||
loggerController.handleGET(request);
|
||||
loggerController.getLogs(query).then(function(res) {
|
||||
expect(res.length).not.toBe(0);
|
||||
done();
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
fail("should not fail");
|
||||
done();
|
||||
})
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('properly validates dateTimes', (done) => {
|
||||
expect(LoggerController.validDateTime()).toBe(null);
|
||||
expect(LoggerController.validDateTime("String")).toBe(null);
|
||||
expect(LoggerController.validDateTime(123456).getTime()).toBe(123456);
|
||||
expect(LoggerController.validDateTime("2016-01-01Z00:00:00").getTime()).toBe(1451606400000);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can check invalid construction of controller', (done) => {
|
||||
it('can set the proper default values', (done) => {
|
||||
// Make mock request
|
||||
var request = {
|
||||
auth: {
|
||||
isMaster: true
|
||||
},
|
||||
query: {}
|
||||
var result = LoggerController.parseOptions();
|
||||
expect(result.size).toEqual(10);
|
||||
expect(result.order).toEqual('desc');
|
||||
expect(result.level).toEqual('info');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('can process a query witout throwing', (done) => {
|
||||
// Make mock request
|
||||
var query = {
|
||||
from: "2016-01-01Z00:00:00",
|
||||
until: "2016-01-01Z00:00:00",
|
||||
size: 5,
|
||||
order: 'asc',
|
||||
level: 'error'
|
||||
};
|
||||
|
||||
var loggerController = new LoggerController();
|
||||
var result = LoggerController.parseOptions(query);
|
||||
|
||||
expect(result.from.getTime()).toEqual(1451606400000);
|
||||
expect(result.until.getTime()).toEqual(1451606400000);
|
||||
expect(result.size).toEqual(5);
|
||||
expect(result.order).toEqual('asc');
|
||||
expect(result.level).toEqual('error');
|
||||
|
||||
expect(() => {
|
||||
loggerController.handleGET(request);
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can check invalid master key of request', (done) => {
|
||||
it('can check process a query witout throwing', (done) => {
|
||||
// Make mock request
|
||||
var request = {
|
||||
auth: {
|
||||
isMaster: false
|
||||
},
|
||||
query: {}
|
||||
var query = {
|
||||
from: "2016-01-01",
|
||||
until: "2016-01-30",
|
||||
size: 5,
|
||||
order: 'desc',
|
||||
level: 'error'
|
||||
};
|
||||
|
||||
var loggerController = new LoggerController(new FileLoggerAdapter());
|
||||
|
||||
expect(() => {
|
||||
loggerController.handleGET(request);
|
||||
loggerController.getLogs(query).then(function(res) {
|
||||
expect(res.length).toBe(0);
|
||||
done();
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
fail("should not fail");
|
||||
done();
|
||||
})
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw without an adapter', (done) => {
|
||||
expect(() => {
|
||||
var loggerController = new LoggerController();
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
'use strict';
|
||||
|
||||
const request = require('request');
|
||||
var LogsRouter = require('../src/Routers/LogsRouter').LogsRouter;
|
||||
var LoggerController = require('../src/Controllers/LoggerController').LoggerController;
|
||||
var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter;
|
||||
|
||||
const loggerController = new LoggerController(new FileLoggerAdapter());
|
||||
|
||||
describe('LogsRouter', () => {
|
||||
it('can check valid master key of request', (done) => {
|
||||
// Make mock request
|
||||
var request = {
|
||||
auth: {
|
||||
isMaster: true
|
||||
},
|
||||
query: {},
|
||||
config: {
|
||||
loggerController: loggerController
|
||||
}
|
||||
};
|
||||
|
||||
var router = new LogsRouter();
|
||||
|
||||
expect(() => {
|
||||
router.validateRequest(request);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can check invalid construction of controller', (done) => {
|
||||
// Make mock request
|
||||
var request = {
|
||||
auth: {
|
||||
isMaster: true
|
||||
},
|
||||
query: {},
|
||||
config: {
|
||||
loggerController: undefined // missing controller
|
||||
}
|
||||
};
|
||||
|
||||
var router = new LogsRouter();
|
||||
|
||||
expect(() => {
|
||||
router.validateRequest(request);
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can check invalid master key of request', done => {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/scriptlog',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(403);
|
||||
expect(body.error).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = function(options) {
|
||||
return {
|
||||
options: options
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
module.exports = options => {
|
||||
if (!options) {
|
||||
throw "Options were not provided"
|
||||
}
|
||||
return {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
|
||||
const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
|
||||
const MongoClient = require('mongodb').MongoClient;
|
||||
|
||||
describe('MongoStorageAdapter', () => {
|
||||
it('auto-escapes symbols in auth information', () => {
|
||||
spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(null));
|
||||
new MongoStorageAdapter('mongodb://user!with@+ symbols:password!with@+ symbols@localhost:1234/parse', {})
|
||||
.connect();
|
||||
expect(MongoClient.connect).toHaveBeenCalledWith(
|
||||
'mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it("doesn't double escape already URI-encoded information", () => {
|
||||
spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(null));
|
||||
new MongoStorageAdapter('mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse', {})
|
||||
.connect();
|
||||
expect(MongoClient.connect).toHaveBeenCalledWith(
|
||||
'mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
// https://github.com/ParsePlatform/parse-server/pull/148#issuecomment-180407057
|
||||
it('preserves replica sets', () => {
|
||||
spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(null));
|
||||
new MongoStorageAdapter('mongodb://test:testpass@ds056315-a0.mongolab.com:59325,ds059315-a1.mongolab.com:59315/testDBname?replicaSet=rs-ds059415', {})
|
||||
.connect();
|
||||
expect(MongoClient.connect).toHaveBeenCalledWith(
|
||||
'mongodb://test:testpass@ds056315-a0.mongolab.com:59325,ds059315-a1.mongolab.com:59315/testDBname?replicaSet=rs-ds059415',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
+50
-31
@@ -1,5 +1,6 @@
|
||||
var OAuth = require("../src/oauth/OAuth1Client");
|
||||
var OAuth = require("../src/authDataManager/OAuth1Client");
|
||||
var request = require('request');
|
||||
var Config = require("../src/Config");
|
||||
|
||||
describe('OAuth', function() {
|
||||
|
||||
@@ -28,7 +29,7 @@ describe('OAuth', function() {
|
||||
|
||||
it("Should properly generate request signature", (done) => {
|
||||
var request = {
|
||||
host: "dummy.com",
|
||||
host: "dummy.com",
|
||||
path: "path"
|
||||
};
|
||||
|
||||
@@ -48,7 +49,7 @@ describe('OAuth', function() {
|
||||
|
||||
it("Should properly build request", (done) => {
|
||||
var options = {
|
||||
host: "dummy.com",
|
||||
host: "dummy.com",
|
||||
consumer_key: "hello",
|
||||
consumer_secret: "world",
|
||||
auth_token: "token",
|
||||
@@ -86,7 +87,7 @@ describe('OAuth', function() {
|
||||
|
||||
it("Should fail a GET request", (done) => {
|
||||
var options = {
|
||||
host: "api.twitter.com",
|
||||
host: "api.twitter.com",
|
||||
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
};
|
||||
@@ -100,7 +101,7 @@ describe('OAuth', function() {
|
||||
|
||||
it("Should fail a POST request", (done) => {
|
||||
var options = {
|
||||
host: "api.twitter.com",
|
||||
host: "api.twitter.com",
|
||||
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
};
|
||||
@@ -117,7 +118,7 @@ describe('OAuth', function() {
|
||||
|
||||
it("Should fail a request", (done) => {
|
||||
var options = {
|
||||
host: "localhost",
|
||||
host: "localhost",
|
||||
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
};
|
||||
@@ -138,11 +139,11 @@ describe('OAuth', function() {
|
||||
|
||||
["facebook", "github", "instagram", "google", "linkedin", "meetup", "twitter"].map(function(providerName){
|
||||
it("Should validate structure of "+providerName, (done) => {
|
||||
var provider = require("../src/oauth/"+providerName);
|
||||
var provider = require("../src/authDataManager/"+providerName);
|
||||
jequal(typeof provider.validateAuthData, "function");
|
||||
jequal(typeof provider.validateAppId, "function");
|
||||
jequal(provider.validateAuthData({}, {}).constructor, Promise.prototype.constructor);
|
||||
jequal(provider.validateAppId("app", "key", {}).constructor, Promise.prototype.constructor);
|
||||
jequal(provider.validateAuthData({}, {}).constructor, Promise.prototype.constructor);
|
||||
jequal(provider.validateAppId("app", "key", {}).constructor, Promise.prototype.constructor);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -203,13 +204,11 @@ describe('OAuth', function() {
|
||||
myoauth: getMockMyOauthProvider().authData
|
||||
}
|
||||
};
|
||||
var headers = {'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'Content-Type': 'application/json' }
|
||||
|
||||
var options = {
|
||||
headers: {'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Installation-Id': 'yolo',
|
||||
'Content-Type': 'application/json' },
|
||||
url: 'http://localhost:8378/1/users',
|
||||
body: JSON.stringify(jsonBody)
|
||||
@@ -223,9 +222,19 @@ describe('OAuth', function() {
|
||||
createOAuthUser((error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
ok(b.sessionToken);
|
||||
expect(b.objectId).not.toBeNull();
|
||||
expect(b.objectId).not.toBeUndefined();
|
||||
done();
|
||||
var sessionToken = b.sessionToken;
|
||||
var q = new Parse.Query("_Session");
|
||||
q.equalTo('sessionToken', sessionToken);
|
||||
q.first({useMasterKey: true}).then((res) => {
|
||||
expect(res.get("installationId")).toEqual('yolo');
|
||||
done();
|
||||
}).fail((err) => {
|
||||
fail('should not fail fetching the session');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
@@ -248,7 +257,7 @@ describe('OAuth', function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
it("unlink and link with custom provider", (done) => {
|
||||
@@ -266,28 +275,38 @@ describe('OAuth', function() {
|
||||
|
||||
model._unlinkFrom("myoauth", {
|
||||
success: function(model) {
|
||||
|
||||
ok(!model._isLinked("myoauth"),
|
||||
"User should not be linked to myoauth");
|
||||
ok(!provider.synchronizedUserId, "User id should be cleared");
|
||||
ok(!provider.synchronizedAuthToken, "Auth token should be cleared");
|
||||
ok(!provider.synchronizedExpiration,
|
||||
"Expiration should be cleared");
|
||||
// make sure the auth data is properly deleted
|
||||
var config = new Config(Parse.applicationId);
|
||||
config.database.mongoFind('_User', {
|
||||
_id: model.id
|
||||
}).then((res) => {
|
||||
expect(res.length).toBe(1);
|
||||
expect(res[0]._auth_data_myoauth).toBeUndefined();
|
||||
expect(res[0]._auth_data_myoauth).not.toBeNull();
|
||||
|
||||
model._linkWith("myoauth", {
|
||||
success: function(model) {
|
||||
ok(provider.synchronizedUserId, "User id should have a value");
|
||||
ok(provider.synchronizedAuthToken,
|
||||
"Auth token should have a value");
|
||||
ok(provider.synchronizedExpiration,
|
||||
"Expiration should have a value");
|
||||
ok(model._isLinked("myoauth"),
|
||||
"User should be linked to myoauth");
|
||||
done();
|
||||
},
|
||||
error: function(model, error) {
|
||||
ok(false, "linking again should succeed");
|
||||
done();
|
||||
}
|
||||
model._linkWith("myoauth", {
|
||||
success: function(model) {
|
||||
ok(provider.synchronizedUserId, "User id should have a value");
|
||||
ok(provider.synchronizedAuthToken,
|
||||
"Auth token should have a value");
|
||||
ok(provider.synchronizedExpiration,
|
||||
"Expiration should have a value");
|
||||
ok(model._isLinked("myoauth"),
|
||||
"User should be linked to myoauth");
|
||||
done();
|
||||
},
|
||||
error: function(model, error) {
|
||||
ok(false, "linking again should succeed");
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
error: function(model, error) {
|
||||
@@ -304,4 +323,4 @@ describe('OAuth', function() {
|
||||
});
|
||||
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
|
||||
var OneSignalPushAdapter = require('../src/Adapters/Push/OneSignalPushAdapter');
|
||||
|
||||
describe('OneSignalPushAdapter', () => {
|
||||
it('can be initialized', (done) => {
|
||||
// Make mock config
|
||||
var pushConfig = {
|
||||
oneSignalAppId:"APP ID",
|
||||
oneSignalApiKey:"API KEY"
|
||||
};
|
||||
|
||||
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
|
||||
|
||||
var senderMap = oneSignalPushAdapter.senderMap;
|
||||
|
||||
expect(senderMap.ios instanceof Function).toBe(true);
|
||||
expect(senderMap.android instanceof Function).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can get valid push types', (done) => {
|
||||
var oneSignalPushAdapter = new OneSignalPushAdapter();
|
||||
|
||||
expect(oneSignalPushAdapter.getValidPushTypes()).toEqual(['ios', 'android']);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can classify installation', (done) => {
|
||||
// Mock installations
|
||||
var validPushTypes = ['ios', 'android'];
|
||||
var installations = [
|
||||
{
|
||||
deviceType: 'android',
|
||||
deviceToken: 'androidToken'
|
||||
},
|
||||
{
|
||||
deviceType: 'ios',
|
||||
deviceToken: 'iosToken'
|
||||
},
|
||||
{
|
||||
deviceType: 'win',
|
||||
deviceToken: 'winToken'
|
||||
},
|
||||
{
|
||||
deviceType: 'android',
|
||||
deviceToken: undefined
|
||||
}
|
||||
];
|
||||
|
||||
var deviceMap = OneSignalPushAdapter.classifyInstallation(installations, validPushTypes);
|
||||
expect(deviceMap['android']).toEqual([makeDevice('androidToken')]);
|
||||
expect(deviceMap['ios']).toEqual([makeDevice('iosToken')]);
|
||||
expect(deviceMap['win']).toBe(undefined);
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
it('can send push notifications', (done) => {
|
||||
var oneSignalPushAdapter = new OneSignalPushAdapter();
|
||||
|
||||
// Mock android ios senders
|
||||
var androidSender = jasmine.createSpy('send')
|
||||
var iosSender = jasmine.createSpy('send')
|
||||
|
||||
var senderMap = {
|
||||
ios: iosSender,
|
||||
android: androidSender
|
||||
};
|
||||
oneSignalPushAdapter.senderMap = senderMap;
|
||||
|
||||
// Mock installations
|
||||
var installations = [
|
||||
{
|
||||
deviceType: 'android',
|
||||
deviceToken: 'androidToken'
|
||||
},
|
||||
{
|
||||
deviceType: 'ios',
|
||||
deviceToken: 'iosToken'
|
||||
},
|
||||
{
|
||||
deviceType: 'win',
|
||||
deviceToken: 'winToken'
|
||||
},
|
||||
{
|
||||
deviceType: 'android',
|
||||
deviceToken: undefined
|
||||
}
|
||||
];
|
||||
var data = {};
|
||||
|
||||
oneSignalPushAdapter.send(data, installations);
|
||||
// Check android sender
|
||||
expect(androidSender).toHaveBeenCalled();
|
||||
var args = androidSender.calls.first().args;
|
||||
expect(args[0]).toEqual(data);
|
||||
expect(args[1]).toEqual([
|
||||
makeDevice('androidToken')
|
||||
]);
|
||||
// Check ios sender
|
||||
expect(iosSender).toHaveBeenCalled();
|
||||
args = iosSender.calls.first().args;
|
||||
expect(args[0]).toEqual(data);
|
||||
expect(args[1]).toEqual([
|
||||
makeDevice('iosToken')
|
||||
]);
|
||||
done();
|
||||
});
|
||||
|
||||
it("can send iOS notifications", (done) => {
|
||||
var oneSignalPushAdapter = new OneSignalPushAdapter();
|
||||
var sendToOneSignal = jasmine.createSpy('sendToOneSignal');
|
||||
oneSignalPushAdapter.sendToOneSignal = sendToOneSignal;
|
||||
|
||||
oneSignalPushAdapter.sendToAPNS({'data':{
|
||||
'badge': 1,
|
||||
'alert': "Example content",
|
||||
'sound': "Example sound",
|
||||
'content-available': 1,
|
||||
'misc-data': 'Example Data'
|
||||
}},[{'deviceToken':'iosToken1'},{'deviceToken':'iosToken2'}])
|
||||
|
||||
expect(sendToOneSignal).toHaveBeenCalled();
|
||||
var args = sendToOneSignal.calls.first().args;
|
||||
expect(args[0]).toEqual({
|
||||
'ios_badgeType':'SetTo',
|
||||
'ios_badgeCount':1,
|
||||
'contents': { 'en':'Example content'},
|
||||
'ios_sound': 'Example sound',
|
||||
'content_available':true,
|
||||
'data':{'misc-data':'Example Data'},
|
||||
'include_ios_tokens':['iosToken1','iosToken2']
|
||||
})
|
||||
done();
|
||||
});
|
||||
|
||||
it("can send Android notifications", (done) => {
|
||||
var oneSignalPushAdapter = new OneSignalPushAdapter();
|
||||
var sendToOneSignal = jasmine.createSpy('sendToOneSignal');
|
||||
oneSignalPushAdapter.sendToOneSignal = sendToOneSignal;
|
||||
|
||||
oneSignalPushAdapter.sendToGCM({'data':{
|
||||
'title': 'Example title',
|
||||
'alert': 'Example content',
|
||||
'misc-data': 'Example Data'
|
||||
}},[{'deviceToken':'androidToken1'},{'deviceToken':'androidToken2'}])
|
||||
|
||||
expect(sendToOneSignal).toHaveBeenCalled();
|
||||
var args = sendToOneSignal.calls.first().args;
|
||||
expect(args[0]).toEqual({
|
||||
'contents': { 'en':'Example content'},
|
||||
'title': {'en':'Example title'},
|
||||
'data':{'misc-data':'Example Data'},
|
||||
'include_android_reg_ids': ['androidToken1','androidToken2']
|
||||
})
|
||||
done();
|
||||
});
|
||||
|
||||
it("can post the correct data", (done) => {
|
||||
var pushConfig = {
|
||||
oneSignalAppId:"APP ID",
|
||||
oneSignalApiKey:"API KEY"
|
||||
};
|
||||
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
|
||||
|
||||
var write = jasmine.createSpy('write');
|
||||
oneSignalPushAdapter.https = {
|
||||
'request': function(a,b) {
|
||||
return {
|
||||
'end':function(){},
|
||||
'on':function(a,b){},
|
||||
'write':write
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var installations = [
|
||||
{
|
||||
deviceType: 'android',
|
||||
deviceToken: 'androidToken'
|
||||
},
|
||||
{
|
||||
deviceType: 'ios',
|
||||
deviceToken: 'iosToken'
|
||||
},
|
||||
{
|
||||
deviceType: 'win',
|
||||
deviceToken: 'winToken'
|
||||
},
|
||||
{
|
||||
deviceType: 'android',
|
||||
deviceToken: undefined
|
||||
}
|
||||
];
|
||||
|
||||
oneSignalPushAdapter.send({'data':{
|
||||
'title': 'Example title',
|
||||
'alert': 'Example content',
|
||||
'content-available':1,
|
||||
'misc-data': 'Example Data'
|
||||
}}, installations);
|
||||
|
||||
expect(write).toHaveBeenCalled();
|
||||
|
||||
// iOS
|
||||
args = write.calls.first().args;
|
||||
expect(args[0]).toEqual(JSON.stringify({
|
||||
'contents': { 'en':'Example content'},
|
||||
'content_available':true,
|
||||
'data':{'title':'Example title','misc-data':'Example Data'},
|
||||
'include_ios_tokens':['iosToken'],
|
||||
'app_id':'APP ID'
|
||||
}));
|
||||
|
||||
// Android
|
||||
args = write.calls.mostRecent().args;
|
||||
expect(args[0]).toEqual(JSON.stringify({
|
||||
'contents': { 'en':'Example content'},
|
||||
'title': {'en':'Example title'},
|
||||
'data':{"content-available":1,'misc-data':'Example Data'},
|
||||
'include_android_reg_ids':['androidToken'],
|
||||
'app_id':'APP ID'
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
function makeDevice(deviceToken, appIdentifier) {
|
||||
return {
|
||||
deviceToken: deviceToken
|
||||
};
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
describe('Parse.Push', () => {
|
||||
|
||||
var setup = function() {
|
||||
var pushAdapter = {
|
||||
send: function(body, installations) {
|
||||
var badge = body.data.badge;
|
||||
let promises = installations.map((installation) => {
|
||||
if (installation.deviceType == "ios") {
|
||||
expect(installation.badge).toEqual(badge);
|
||||
expect(installation.originalBadge+1).toEqual(installation.badge);
|
||||
} else {
|
||||
expect(installation.badge).toBeUndefined();
|
||||
}
|
||||
return Promise.resolve({
|
||||
err: null,
|
||||
deviceType: installation.deviceType,
|
||||
result: true
|
||||
})
|
||||
});
|
||||
return Promise.all(promises);
|
||||
},
|
||||
getValidPushTypes: function() {
|
||||
return ["ios", "android"];
|
||||
}
|
||||
}
|
||||
|
||||
setServerConfiguration({
|
||||
appId: Parse.applicationId,
|
||||
masterKey: Parse.masterKey,
|
||||
serverURL: Parse.serverURL,
|
||||
push: {
|
||||
adapter: pushAdapter
|
||||
}
|
||||
});
|
||||
|
||||
var installations = [];
|
||||
while(installations.length != 10) {
|
||||
var installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_"+installations.length);
|
||||
installation.set("deviceToken","device_token_"+installations.length)
|
||||
installation.set("badge", installations.length);
|
||||
installation.set("originalBadge", installations.length);
|
||||
installation.set("deviceType", "ios");
|
||||
installations.push(installation);
|
||||
}
|
||||
return Parse.Object.saveAll(installations);
|
||||
}
|
||||
|
||||
it('should properly send push', (done) => {
|
||||
return setup().then(() => {
|
||||
return Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios'
|
||||
},
|
||||
data: {
|
||||
badge: 'Increment',
|
||||
alert: 'Hello world!'
|
||||
}
|
||||
}, {useMasterKey: true})
|
||||
})
|
||||
.then(() => {
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error();
|
||||
fail('should not fail sending push')
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly send push with lowercaseIncrement', (done) => {
|
||||
return setup().then(() => {
|
||||
return Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios'
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!'
|
||||
}
|
||||
}, {useMasterKey: true})
|
||||
}).then(() => {
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error();
|
||||
fail('should not fail sending push')
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
+52
-1
@@ -1,5 +1,9 @@
|
||||
// This is a port of the test suite:
|
||||
// hungry/js/test/parse_acl_test.js
|
||||
var rest = require('../src/rest');
|
||||
var Config = require('../src/Config');
|
||||
var config = new Config('test');
|
||||
var auth = require('../src/Auth');
|
||||
|
||||
describe('Parse.ACL', () => {
|
||||
it("acl must be valid", (done) => {
|
||||
@@ -786,7 +790,11 @@ describe('Parse.ACL', () => {
|
||||
equal(results.length, 1);
|
||||
var result = results[0];
|
||||
ok(result);
|
||||
equal(result.id, object.id);
|
||||
if (!result) {
|
||||
fail("should have result");
|
||||
} else {
|
||||
equal(result.id, object.id);
|
||||
}
|
||||
done();
|
||||
}
|
||||
});
|
||||
@@ -1141,4 +1149,47 @@ describe('Parse.ACL', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it('restricted ACL does not have public access', (done) => {
|
||||
var obj = new Parse.Object("TestClassMasterACL");
|
||||
var acl = new Parse.ACL();
|
||||
obj.set('ACL', acl);
|
||||
obj.save().then(() => {
|
||||
var query = new Parse.Query("TestClassMasterACL");
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
ok(!results.length, 'Should not have returned object with secure ACL.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('regression test #701', done => {
|
||||
var anonUser = {
|
||||
authData: {
|
||||
anonymous: {
|
||||
id: '00000000-0000-0000-0000-000000000001'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Parse.Cloud.afterSave(Parse.User, req => {
|
||||
if (!req.object.existed()) {
|
||||
var user = req.object;
|
||||
var acl = new Parse.ACL(user);
|
||||
user.setACL(acl);
|
||||
user.save(null, {useMasterKey: true}).then(user => {
|
||||
new Parse.Query('_User').get(user.objectId).then(user => {
|
||||
fail('should not have fetched user without public read enabled');
|
||||
done();
|
||||
}, error => {
|
||||
expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
|
||||
Parse.Cloud._removeHook('Triggers', 'afterSave', Parse.User.className);
|
||||
done();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
rest.create(config, auth.nobody(config), '_User', anonUser)
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
+653
-103
@@ -1,8 +1,10 @@
|
||||
// A bunch of different tests are in here - it isn't very thematic.
|
||||
// It would probably be better to refactor them into different files.
|
||||
'use strict';
|
||||
|
||||
var DatabaseAdapter = require('../src/DatabaseAdapter');
|
||||
var request = require('request');
|
||||
const Parse = require("parse/node");
|
||||
|
||||
describe('miscellaneous', function() {
|
||||
it('create a GameScore object', function(done) {
|
||||
@@ -138,7 +140,7 @@ describe('miscellaneous', function() {
|
||||
return new Parse.Query(TestObject).find();
|
||||
}).then((results) => {
|
||||
expect(results.length).toEqual(100);
|
||||
done();
|
||||
done();
|
||||
}, (error) => {
|
||||
fail(error);
|
||||
done();
|
||||
@@ -222,6 +224,19 @@ describe('miscellaneous', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('test beforeSave returns value on create and update', (done) => {
|
||||
var obj = new Parse.Object('BeforeSaveChanged');
|
||||
obj.set('foo', 'bing');
|
||||
obj.save().then(() => {
|
||||
expect(obj.get('foo')).toEqual('baz');
|
||||
obj.set('foo', 'bar');
|
||||
return obj.save().then(() => {
|
||||
expect(obj.get('foo')).toEqual('baz');
|
||||
done();
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
it('test afterSave ran and created an object', function(done) {
|
||||
var obj = new Parse.Object('AfterSaveTest');
|
||||
obj.save();
|
||||
@@ -274,13 +289,17 @@ describe('miscellaneous', function() {
|
||||
var objAgain = new Parse.Object('BeforeDeleteFail', {objectId: id});
|
||||
return objAgain.fetch();
|
||||
}).then((objAgain) => {
|
||||
expect(objAgain.get('foo')).toEqual('bar');
|
||||
if (objAgain) {
|
||||
expect(objAgain.get('foo')).toEqual('bar');
|
||||
} else {
|
||||
fail("unable to fetch the object ", id);
|
||||
}
|
||||
done();
|
||||
}, (error) => {
|
||||
// We should have been able to fetch the object again
|
||||
fail(error);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('basic beforeDelete rejection via promise', function(done) {
|
||||
var obj = new Parse.Object('BeforeDeleteFailWithPromise');
|
||||
@@ -350,6 +369,11 @@ describe('miscellaneous', function() {
|
||||
it('test cloud function return types', function(done) {
|
||||
Parse.Cloud.run('foo').then((result) => {
|
||||
expect(result.object instanceof Parse.Object).toBeTruthy();
|
||||
if (!result.object) {
|
||||
fail("Unable to run foo");
|
||||
done();
|
||||
return;
|
||||
}
|
||||
expect(result.object.className).toEqual('Foo');
|
||||
expect(result.object.get('x')).toEqual(2);
|
||||
var bar = result.object.get('relation');
|
||||
@@ -363,6 +387,22 @@ describe('miscellaneous', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('test cloud function should echo keys', function(done) {
|
||||
Parse.Cloud.run('echoKeys').then((result) => {
|
||||
expect(result.applicationId).toEqual(Parse.applicationId);
|
||||
expect(result.masterKey).toEqual(Parse.masterKey);
|
||||
expect(result.javascriptKey).toEqual(Parse.javascriptKey);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly create an object in before save', (done) => {
|
||||
Parse.Cloud.run('createBeforeSaveChangedObject').then((res) => {
|
||||
expect(res.get('foo')).toEqual('baz');
|
||||
done();
|
||||
});
|
||||
})
|
||||
|
||||
it('test rest_create_app', function(done) {
|
||||
var appId;
|
||||
Parse._request('POST', 'rest_create_app').then((res) => {
|
||||
@@ -374,55 +414,239 @@ describe('miscellaneous', function() {
|
||||
obj.set('foo', 'bar');
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
var db = DatabaseAdapter.getDatabaseConnection(appId);
|
||||
var db = DatabaseAdapter.getDatabaseConnection(appId, 'test_');
|
||||
return db.mongoFind('TestObject', {}, {});
|
||||
}).then((results) => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0]['foo']).toEqual('bar');
|
||||
done();
|
||||
}).fail(err => {
|
||||
fail(err);
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
describe('beforeSave', () => {
|
||||
beforeEach(done => {
|
||||
// Make sure the required mock for all tests is unset.
|
||||
Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore");
|
||||
done();
|
||||
});
|
||||
afterEach(done => {
|
||||
// Make sure the required mock for all tests is unset.
|
||||
Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore");
|
||||
done();
|
||||
});
|
||||
|
||||
it('object is set on create and update', done => {
|
||||
let triggerTime = 0;
|
||||
// Register a mock beforeSave hook
|
||||
Parse.Cloud.beforeSave('GameScore', (req, res) => {
|
||||
let object = req.object;
|
||||
expect(object instanceof Parse.Object).toBeTruthy();
|
||||
expect(object.get('fooAgain')).toEqual('barAgain');
|
||||
if (triggerTime == 0) {
|
||||
// Create
|
||||
expect(object.get('foo')).toEqual('bar');
|
||||
// No objectId/createdAt/updatedAt
|
||||
expect(object.id).toBeUndefined();
|
||||
expect(object.createdAt).toBeUndefined();
|
||||
expect(object.updatedAt).toBeUndefined();
|
||||
} else if (triggerTime == 1) {
|
||||
// Update
|
||||
expect(object.get('foo')).toEqual('baz');
|
||||
expect(object.id).not.toBeUndefined();
|
||||
expect(object.createdAt).not.toBeUndefined();
|
||||
expect(object.updatedAt).not.toBeUndefined();
|
||||
} else {
|
||||
res.error();
|
||||
}
|
||||
triggerTime++;
|
||||
res.success();
|
||||
});
|
||||
|
||||
let obj = new Parse.Object('GameScore');
|
||||
obj.set('foo', 'bar');
|
||||
obj.set('fooAgain', 'barAgain');
|
||||
obj.save().then(() => {
|
||||
// We only update foo
|
||||
obj.set('foo', 'baz');
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
// Make sure the checking has been triggered
|
||||
expect(triggerTime).toBe(2);
|
||||
done();
|
||||
}, error => {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('dirtyKeys are set on update', done => {
|
||||
let triggerTime = 0;
|
||||
// Register a mock beforeSave hook
|
||||
Parse.Cloud.beforeSave('GameScore', (req, res) => {
|
||||
var object = req.object;
|
||||
expect(object instanceof Parse.Object).toBeTruthy();
|
||||
expect(object.get('fooAgain')).toEqual('barAgain');
|
||||
if (triggerTime == 0) {
|
||||
// Create
|
||||
expect(object.get('foo')).toEqual('bar');
|
||||
} else if (triggerTime == 1) {
|
||||
// Update
|
||||
expect(object.dirtyKeys()).toEqual(['foo']);
|
||||
expect(object.dirty('foo')).toBeTruthy();
|
||||
expect(object.get('foo')).toEqual('baz');
|
||||
} else {
|
||||
res.error();
|
||||
}
|
||||
triggerTime++;
|
||||
res.success();
|
||||
});
|
||||
|
||||
let obj = new Parse.Object('GameScore');
|
||||
obj.set('foo', 'bar');
|
||||
obj.set('fooAgain', 'barAgain');
|
||||
obj.save().then(() => {
|
||||
// We only update foo
|
||||
obj.set('foo', 'baz');
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
// Make sure the checking has been triggered
|
||||
expect(triggerTime).toBe(2);
|
||||
done();
|
||||
}, function(error) {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('original object is set on update', done => {
|
||||
let triggerTime = 0;
|
||||
// Register a mock beforeSave hook
|
||||
Parse.Cloud.beforeSave('GameScore', (req, res) => {
|
||||
let object = req.object;
|
||||
expect(object instanceof Parse.Object).toBeTruthy();
|
||||
expect(object.get('fooAgain')).toEqual('barAgain');
|
||||
let originalObject = req.original;
|
||||
if (triggerTime == 0) {
|
||||
// No id/createdAt/updatedAt
|
||||
expect(object.id).toBeUndefined();
|
||||
expect(object.createdAt).toBeUndefined();
|
||||
expect(object.updatedAt).toBeUndefined();
|
||||
// Create
|
||||
expect(object.get('foo')).toEqual('bar');
|
||||
// Check the originalObject is undefined
|
||||
expect(originalObject).toBeUndefined();
|
||||
} else if (triggerTime == 1) {
|
||||
// Update
|
||||
expect(object.id).not.toBeUndefined();
|
||||
expect(object.createdAt).not.toBeUndefined();
|
||||
expect(object.updatedAt).not.toBeUndefined();
|
||||
expect(object.get('foo')).toEqual('baz');
|
||||
// Check the originalObject
|
||||
expect(originalObject instanceof Parse.Object).toBeTruthy();
|
||||
expect(originalObject.get('fooAgain')).toEqual('barAgain');
|
||||
expect(originalObject.id).not.toBeUndefined();
|
||||
expect(originalObject.createdAt).not.toBeUndefined();
|
||||
expect(originalObject.updatedAt).not.toBeUndefined();
|
||||
expect(originalObject.get('foo')).toEqual('bar');
|
||||
} else {
|
||||
res.error();
|
||||
}
|
||||
triggerTime++;
|
||||
res.success();
|
||||
});
|
||||
|
||||
let obj = new Parse.Object('GameScore');
|
||||
obj.set('foo', 'bar');
|
||||
obj.set('fooAgain', 'barAgain');
|
||||
obj.save().then(() => {
|
||||
// We only update foo
|
||||
obj.set('foo', 'baz');
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
// Make sure the checking has been triggered
|
||||
expect(triggerTime).toBe(2);
|
||||
done();
|
||||
}, error => {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('pointer mutation properly saves object', done => {
|
||||
let className = 'GameScore';
|
||||
|
||||
Parse.Cloud.beforeSave(className, (req, res) => {
|
||||
let object = req.object;
|
||||
expect(object instanceof Parse.Object).toBeTruthy();
|
||||
|
||||
let child = object.get('child');
|
||||
expect(child instanceof Parse.Object).toBeTruthy();
|
||||
child.set('a', 'b');
|
||||
child.save().then(() => {
|
||||
res.success();
|
||||
});
|
||||
});
|
||||
|
||||
let obj = new Parse.Object(className);
|
||||
obj.set('foo', 'bar');
|
||||
|
||||
let child = new Parse.Object('Child');
|
||||
child.save().then(() => {
|
||||
obj.set('child', child);
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
let query = new Parse.Query(className);
|
||||
query.include('child');
|
||||
return query.get(obj.id).then(objAgain => {
|
||||
expect(objAgain.get('foo')).toEqual('bar');
|
||||
|
||||
let childAgain = objAgain.get('child');
|
||||
expect(childAgain instanceof Parse.Object).toBeTruthy();
|
||||
expect(childAgain.get('a')).toEqual('b');
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
}).then(() => {
|
||||
done();
|
||||
}, error => {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('test beforeSave get full object on create and update', function(done) {
|
||||
var triggerTime = 0;
|
||||
// Register a mock beforeSave hook
|
||||
Parse.Cloud.beforeSave('GameScore', function(req, res) {
|
||||
var object = req.object;
|
||||
expect(object instanceof Parse.Object).toBeTruthy();
|
||||
expect(object.get('fooAgain')).toEqual('barAgain');
|
||||
expect(object.id).not.toBeUndefined();
|
||||
expect(object.createdAt).not.toBeUndefined();
|
||||
expect(object.updatedAt).not.toBeUndefined();
|
||||
if (triggerTime == 0) {
|
||||
// Create
|
||||
expect(object.get('foo')).toEqual('bar');
|
||||
} else if (triggerTime == 1) {
|
||||
// Update
|
||||
expect(object.get('foo')).toEqual('baz');
|
||||
} else {
|
||||
res.error();
|
||||
}
|
||||
triggerTime++;
|
||||
res.success();
|
||||
});
|
||||
it('pointer reassign is working properly (#1288)', (done) => {
|
||||
Parse.Cloud.beforeSave('GameScore', (req, res) => {
|
||||
|
||||
var obj = req.object;
|
||||
if (obj.get('point')) {
|
||||
return res.success();
|
||||
}
|
||||
var TestObject1 = Parse.Object.extend('TestObject1');
|
||||
var newObj = new TestObject1({'key1': 1});
|
||||
|
||||
return newObj.save().then((newObj) => {
|
||||
obj.set('point' , newObj);
|
||||
res.success();
|
||||
});
|
||||
});
|
||||
var pointId;
|
||||
var obj = new Parse.Object('GameScore');
|
||||
obj.set('foo', 'bar');
|
||||
obj.set('fooAgain', 'barAgain');
|
||||
obj.save().then(function() {
|
||||
// We only update foo
|
||||
obj.save().then(() => {
|
||||
expect(obj.get('point')).not.toBeUndefined();
|
||||
pointId = obj.get('point').id;
|
||||
expect(pointId).not.toBeUndefined();
|
||||
obj.set('foo', 'baz');
|
||||
return obj.save();
|
||||
}).then(function() {
|
||||
// Make sure the checking has been triggered
|
||||
expect(triggerTime).toBe(2);
|
||||
// Clear mock beforeSave
|
||||
delete Parse.Cloud.Triggers.beforeSave.GameScore;
|
||||
}).then((obj) => {
|
||||
expect(obj.get('point').id).toEqual(pointId);
|
||||
Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore");
|
||||
done();
|
||||
}, function(error) {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('test afterSave get full object on create and update', function(done) {
|
||||
@@ -431,10 +655,10 @@ describe('miscellaneous', function() {
|
||||
Parse.Cloud.afterSave('GameScore', function(req, res) {
|
||||
var object = req.object;
|
||||
expect(object instanceof Parse.Object).toBeTruthy();
|
||||
expect(object.get('fooAgain')).toEqual('barAgain');
|
||||
expect(object.id).not.toBeUndefined();
|
||||
expect(object.createdAt).not.toBeUndefined();
|
||||
expect(object.updatedAt).not.toBeUndefined();
|
||||
expect(object.get('fooAgain')).toEqual('barAgain');
|
||||
if (triggerTime == 0) {
|
||||
// Create
|
||||
expect(object.get('foo')).toEqual('bar');
|
||||
@@ -448,58 +672,6 @@ describe('miscellaneous', function() {
|
||||
res.success();
|
||||
});
|
||||
|
||||
var obj = new Parse.Object('GameScore');
|
||||
obj.set('foo', 'bar');
|
||||
obj.set('fooAgain', 'barAgain');
|
||||
obj.save().then(function() {
|
||||
// We only update foo
|
||||
obj.set('foo', 'baz');
|
||||
return obj.save();
|
||||
}).then(function() {
|
||||
// Make sure the checking has been triggered
|
||||
expect(triggerTime).toBe(2);
|
||||
// Clear mock afterSave
|
||||
delete Parse.Cloud.Triggers.afterSave.GameScore;
|
||||
done();
|
||||
}, function(error) {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('test beforeSave get original object on update', function(done) {
|
||||
var triggerTime = 0;
|
||||
// Register a mock beforeSave hook
|
||||
Parse.Cloud.beforeSave('GameScore', function(req, res) {
|
||||
var object = req.object;
|
||||
expect(object instanceof Parse.Object).toBeTruthy();
|
||||
expect(object.get('fooAgain')).toEqual('barAgain');
|
||||
expect(object.id).not.toBeUndefined();
|
||||
expect(object.createdAt).not.toBeUndefined();
|
||||
expect(object.updatedAt).not.toBeUndefined();
|
||||
var originalObject = req.original;
|
||||
if (triggerTime == 0) {
|
||||
// Create
|
||||
expect(object.get('foo')).toEqual('bar');
|
||||
// Check the originalObject is undefined
|
||||
expect(originalObject).toBeUndefined();
|
||||
} else if (triggerTime == 1) {
|
||||
// Update
|
||||
expect(object.get('foo')).toEqual('baz');
|
||||
// Check the originalObject
|
||||
expect(originalObject instanceof Parse.Object).toBeTruthy();
|
||||
expect(originalObject.get('fooAgain')).toEqual('barAgain');
|
||||
expect(originalObject.id).not.toBeUndefined();
|
||||
expect(originalObject.createdAt).not.toBeUndefined();
|
||||
expect(originalObject.updatedAt).not.toBeUndefined();
|
||||
expect(originalObject.get('foo')).toEqual('bar');
|
||||
} else {
|
||||
res.error();
|
||||
}
|
||||
triggerTime++;
|
||||
res.success();
|
||||
});
|
||||
|
||||
var obj = new Parse.Object('GameScore');
|
||||
obj.set('foo', 'bar');
|
||||
obj.set('fooAgain', 'barAgain');
|
||||
@@ -511,7 +683,7 @@ describe('miscellaneous', function() {
|
||||
// Make sure the checking has been triggered
|
||||
expect(triggerTime).toBe(2);
|
||||
// Clear mock beforeSave
|
||||
delete Parse.Cloud.Triggers.beforeSave.GameScore;
|
||||
Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore");
|
||||
done();
|
||||
}, function(error) {
|
||||
fail(error);
|
||||
@@ -522,6 +694,7 @@ describe('miscellaneous', function() {
|
||||
it('test afterSave get original object on update', function(done) {
|
||||
var triggerTime = 0;
|
||||
// Register a mock beforeSave hook
|
||||
|
||||
Parse.Cloud.afterSave('GameScore', function(req, res) {
|
||||
var object = req.object;
|
||||
expect(object instanceof Parse.Object).toBeTruthy();
|
||||
@@ -563,14 +736,233 @@ describe('miscellaneous', function() {
|
||||
// Make sure the checking has been triggered
|
||||
expect(triggerTime).toBe(2);
|
||||
// Clear mock afterSave
|
||||
delete Parse.Cloud.Triggers.afterSave.GameScore;
|
||||
Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore");
|
||||
done();
|
||||
}, function(error) {
|
||||
console.error(error);
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('test afterSave get full original object even req auth can not query it', (done) => {
|
||||
var triggerTime = 0;
|
||||
// Register a mock beforeSave hook
|
||||
Parse.Cloud.afterSave('GameScore', function(req, res) {
|
||||
var object = req.object;
|
||||
var originalObject = req.original;
|
||||
if (triggerTime == 0) {
|
||||
// Create
|
||||
} else if (triggerTime == 1) {
|
||||
// Update
|
||||
expect(object.get('foo')).toEqual('baz');
|
||||
// Make sure we get the full originalObject
|
||||
expect(originalObject instanceof Parse.Object).toBeTruthy();
|
||||
expect(originalObject.get('fooAgain')).toEqual('barAgain');
|
||||
expect(originalObject.id).not.toBeUndefined();
|
||||
expect(originalObject.createdAt).not.toBeUndefined();
|
||||
expect(originalObject.updatedAt).not.toBeUndefined();
|
||||
expect(originalObject.get('foo')).toEqual('bar');
|
||||
} else {
|
||||
res.error();
|
||||
}
|
||||
triggerTime++;
|
||||
res.success();
|
||||
});
|
||||
|
||||
var obj = new Parse.Object('GameScore');
|
||||
obj.set('foo', 'bar');
|
||||
obj.set('fooAgain', 'barAgain');
|
||||
var acl = new Parse.ACL();
|
||||
// Make sure our update request can not query the object
|
||||
acl.setPublicReadAccess(false);
|
||||
acl.setPublicWriteAccess(true);
|
||||
obj.setACL(acl);
|
||||
obj.save().then(function() {
|
||||
// We only update foo
|
||||
obj.set('foo', 'baz');
|
||||
return obj.save();
|
||||
}).then(function() {
|
||||
// Make sure the checking has been triggered
|
||||
expect(triggerTime).toBe(2);
|
||||
// Clear mock afterSave
|
||||
Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore");
|
||||
done();
|
||||
}, function(error) {
|
||||
console.error(error);
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('afterSave flattens custom operations', done => {
|
||||
var triggerTime = 0;
|
||||
// Register a mock beforeSave hook
|
||||
Parse.Cloud.afterSave('GameScore', function(req, res) {
|
||||
let object = req.object;
|
||||
expect(object instanceof Parse.Object).toBeTruthy();
|
||||
let originalObject = req.original;
|
||||
if (triggerTime == 0) {
|
||||
// Create
|
||||
expect(object.get('yolo')).toEqual(1);
|
||||
} else if (triggerTime == 1) {
|
||||
// Update
|
||||
expect(object.get('yolo')).toEqual(2);
|
||||
// Check the originalObject
|
||||
expect(originalObject.get('yolo')).toEqual(1);
|
||||
} else {
|
||||
res.error();
|
||||
}
|
||||
triggerTime++;
|
||||
res.success();
|
||||
});
|
||||
|
||||
var obj = new Parse.Object('GameScore');
|
||||
obj.increment('yolo', 1);
|
||||
obj.save().then(() => {
|
||||
obj.increment('yolo', 1);
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
// Make sure the checking has been triggered
|
||||
expect(triggerTime).toBe(2);
|
||||
// Clear mock afterSave
|
||||
Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore");
|
||||
done();
|
||||
}, error => {
|
||||
console.error(error);
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('beforeSave receives ACL', done => {
|
||||
let triggerTime = 0;
|
||||
// Register a mock beforeSave hook
|
||||
Parse.Cloud.beforeSave('GameScore', function(req, res) {
|
||||
let object = req.object;
|
||||
if (triggerTime == 0) {
|
||||
let acl = object.getACL();
|
||||
expect(acl.getPublicReadAccess()).toBeTruthy();
|
||||
expect(acl.getPublicWriteAccess()).toBeTruthy();
|
||||
} else if (triggerTime == 1) {
|
||||
let acl = object.getACL();
|
||||
expect(acl.getPublicReadAccess()).toBeFalsy();
|
||||
expect(acl.getPublicWriteAccess()).toBeTruthy();
|
||||
} else {
|
||||
res.error();
|
||||
}
|
||||
triggerTime++;
|
||||
res.success();
|
||||
});
|
||||
|
||||
let obj = new Parse.Object('GameScore');
|
||||
let acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
acl.setPublicWriteAccess(true);
|
||||
obj.setACL(acl);
|
||||
obj.save().then(() => {
|
||||
acl.setPublicReadAccess(false);
|
||||
obj.setACL(acl);
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
// Make sure the checking has been triggered
|
||||
expect(triggerTime).toBe(2);
|
||||
// Clear mock afterSave
|
||||
Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore");
|
||||
done();
|
||||
}, error => {
|
||||
console.error(error);
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('afterSave receives ACL', done => {
|
||||
let triggerTime = 0;
|
||||
// Register a mock beforeSave hook
|
||||
Parse.Cloud.afterSave('GameScore', function(req, res) {
|
||||
let object = req.object;
|
||||
if (triggerTime == 0) {
|
||||
let acl = object.getACL();
|
||||
expect(acl.getPublicReadAccess()).toBeTruthy();
|
||||
expect(acl.getPublicWriteAccess()).toBeTruthy();
|
||||
} else if (triggerTime == 1) {
|
||||
let acl = object.getACL();
|
||||
expect(acl.getPublicReadAccess()).toBeFalsy();
|
||||
expect(acl.getPublicWriteAccess()).toBeTruthy();
|
||||
} else {
|
||||
res.error();
|
||||
}
|
||||
triggerTime++;
|
||||
res.success();
|
||||
});
|
||||
|
||||
let obj = new Parse.Object('GameScore');
|
||||
let acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
acl.setPublicWriteAccess(true);
|
||||
obj.setACL(acl);
|
||||
obj.save().then(() => {
|
||||
acl.setPublicReadAccess(false);
|
||||
obj.setACL(acl);
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
// Make sure the checking has been triggered
|
||||
expect(triggerTime).toBe(2);
|
||||
// Clear mock afterSave
|
||||
Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore");
|
||||
done();
|
||||
}, error => {
|
||||
console.error(error);
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the updated fields on PUT', (done) => {
|
||||
let obj = new Parse.Object('GameScore');
|
||||
obj.save({a:'hello', c: 1, d: ['1'], e:['1'], f:['1','2']}).then(( ) => {
|
||||
var headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Installation-Id': 'yolo'
|
||||
};
|
||||
request.put({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/classes/GameScore/'+obj.id,
|
||||
body: JSON.stringify({
|
||||
a: 'b',
|
||||
c: {"__op":"Increment","amount":2},
|
||||
d: {"__op":"Add", objects: ['2']},
|
||||
e: {"__op":"AddUnique", objects: ['1', '2']},
|
||||
f: {"__op":"Remove", objects: ['2']},
|
||||
selfThing: {"__type":"Pointer","className":"GameScore","objectId":obj.id},
|
||||
})
|
||||
}, (error, response, body) => {
|
||||
body = JSON.parse(body);
|
||||
expect(body.a).toBeUndefined();
|
||||
expect(body.c).toEqual(3); // 2+1
|
||||
expect(body.d.length).toBe(2);
|
||||
expect(body.d.indexOf('1') > -1).toBe(true);
|
||||
expect(body.d.indexOf('2') > -1).toBe(true);
|
||||
expect(body.e.length).toBe(2);
|
||||
expect(body.e.indexOf('1') > -1).toBe(true);
|
||||
expect(body.e.indexOf('2') > -1).toBe(true);
|
||||
expect(body.f.length).toBe(1);
|
||||
expect(body.f.indexOf('1') > -1).toBe(true);
|
||||
// return nothing on other self
|
||||
expect(body.selfThing).toBeUndefined();
|
||||
// updatedAt is always set
|
||||
expect(body.updatedAt).not.toBeUndefined();
|
||||
done();
|
||||
});
|
||||
}).fail((err) => {
|
||||
fail('Should not fail');
|
||||
done();
|
||||
})
|
||||
})
|
||||
|
||||
it('test cloud function error handling', (done) => {
|
||||
// Register a function which will fail
|
||||
Parse.Cloud.define('willFail', (req, res) => {
|
||||
@@ -578,16 +970,90 @@ describe('miscellaneous', function() {
|
||||
});
|
||||
Parse.Cloud.run('willFail').then((s) => {
|
||||
fail('Should not have succeeded.');
|
||||
delete Parse.Cloud.Functions['willFail'];
|
||||
Parse.Cloud._removeHook("Functions", "willFail");
|
||||
done();
|
||||
}, (e) => {
|
||||
expect(e.code).toEqual(141);
|
||||
expect(e.message).toEqual('noway');
|
||||
delete Parse.Cloud.Functions['willFail'];
|
||||
Parse.Cloud._removeHook("Functions", "willFail");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('test beforeSave/afterSave get installationId', function(done) {
|
||||
let triggerTime = 0;
|
||||
Parse.Cloud.beforeSave('GameScore', function(req, res) {
|
||||
triggerTime++;
|
||||
expect(triggerTime).toEqual(1);
|
||||
expect(req.installationId).toEqual('yolo');
|
||||
res.success();
|
||||
});
|
||||
Parse.Cloud.afterSave('GameScore', function(req) {
|
||||
triggerTime++;
|
||||
expect(triggerTime).toEqual(2);
|
||||
expect(req.installationId).toEqual('yolo');
|
||||
});
|
||||
|
||||
var headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Installation-Id': 'yolo'
|
||||
};
|
||||
request.post({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/classes/GameScore',
|
||||
body: JSON.stringify({ a: 'b' })
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
expect(triggerTime).toEqual(2);
|
||||
|
||||
Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore");
|
||||
Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('test beforeDelete/afterDelete get installationId', function(done) {
|
||||
let triggerTime = 0;
|
||||
Parse.Cloud.beforeDelete('GameScore', function(req, res) {
|
||||
triggerTime++;
|
||||
expect(triggerTime).toEqual(1);
|
||||
expect(req.installationId).toEqual('yolo');
|
||||
res.success();
|
||||
});
|
||||
Parse.Cloud.afterDelete('GameScore', function(req) {
|
||||
triggerTime++;
|
||||
expect(triggerTime).toEqual(2);
|
||||
expect(req.installationId).toEqual('yolo');
|
||||
});
|
||||
|
||||
var headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Installation-Id': 'yolo'
|
||||
};
|
||||
request.post({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/classes/GameScore',
|
||||
body: JSON.stringify({ a: 'b' })
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
request.del({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/classes/GameScore/' + JSON.parse(body).objectId
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
expect(triggerTime).toEqual(2);
|
||||
|
||||
Parse.Cloud._removeHook("Triggers", "beforeDelete", "GameScore");
|
||||
Parse.Cloud._removeHook("Triggers", "afterDelete", "GameScore");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('test cloud function query parameters', (done) => {
|
||||
Parse.Cloud.define('echoParams', (req, res) => {
|
||||
res.success(req.params);
|
||||
@@ -612,7 +1078,7 @@ describe('miscellaneous', function() {
|
||||
// Make sure query string params override body params
|
||||
expect(res.other).toEqual('2');
|
||||
expect(res.foo).toEqual("bar");
|
||||
delete Parse.Cloud.Functions['echoParams'];
|
||||
Parse.Cloud._removeHook("Functions",'echoParams');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -621,12 +1087,12 @@ describe('miscellaneous', function() {
|
||||
// Register a function with validation
|
||||
Parse.Cloud.define('functionWithParameterValidation', (req, res) => {
|
||||
res.success('works');
|
||||
}, (params) => {
|
||||
return params.success === 100;
|
||||
}, (request) => {
|
||||
return request.params.success === 100;
|
||||
});
|
||||
|
||||
Parse.Cloud.run('functionWithParameterValidation', {"success":100}).then((s) => {
|
||||
delete Parse.Cloud.Functions['functionWithParameterValidation'];
|
||||
Parse.Cloud._removeHook("Functions", "functionWithParameterValidation");
|
||||
done();
|
||||
}, (e) => {
|
||||
fail('Validation should not have failed.');
|
||||
@@ -638,13 +1104,13 @@ describe('miscellaneous', function() {
|
||||
// Register a function with validation
|
||||
Parse.Cloud.define('functionWithParameterValidationFailure', (req, res) => {
|
||||
res.success('noway');
|
||||
}, (params) => {
|
||||
return params.success === 100;
|
||||
}, (request) => {
|
||||
return request.params.success === 100;
|
||||
});
|
||||
|
||||
Parse.Cloud.run('functionWithParameterValidationFailure', {"success":500}).then((s) => {
|
||||
fail('Validation should not have succeeded');
|
||||
delete Parse.Cloud.Functions['functionWithParameterValidationFailure'];
|
||||
Parse.Cloud._removeHook("Functions", "functionWithParameterValidationFailure");
|
||||
done();
|
||||
}, (e) => {
|
||||
expect(e.code).toEqual(141);
|
||||
@@ -721,4 +1187,88 @@ describe('miscellaneous', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on invalid function', done => {
|
||||
Parse.Cloud.run('somethingThatDoesDefinitelyNotExist').then((s) => {
|
||||
fail('This should have never suceeded');
|
||||
done();
|
||||
}, (e) => {
|
||||
expect(e.code).toEqual(Parse.Error.SCRIPT_FAILED);
|
||||
expect(e.message).toEqual('Invalid function.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('beforeSave change propagates through the save response', (done) => {
|
||||
Parse.Cloud.beforeSave('ChangingObject', function(request, response) {
|
||||
request.object.set('foo', 'baz');
|
||||
response.success();
|
||||
});
|
||||
let obj = new Parse.Object('ChangingObject');
|
||||
obj.save({ foo: 'bar' }).then((objAgain) => {
|
||||
expect(objAgain.get('foo')).toEqual('baz');
|
||||
Parse.Cloud._removeHook("Triggers", "beforeSave", "ChangingObject");
|
||||
done();
|
||||
}, (e) => {
|
||||
Parse.Cloud._removeHook("Triggers", "beforeSave", "ChangingObject");
|
||||
fail('Should not have failed to save.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('dedupes an installation properly and returns updatedAt', (done) => {
|
||||
let headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
};
|
||||
let data = {
|
||||
'installationId': 'lkjsahdfkjhsdfkjhsdfkjhsdf',
|
||||
'deviceType': 'embedded'
|
||||
};
|
||||
let requestOptions = {
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/installations',
|
||||
body: JSON.stringify(data)
|
||||
};
|
||||
request.post(requestOptions, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
let b = JSON.parse(body);
|
||||
expect(typeof b.objectId).toEqual('string');
|
||||
request.post(requestOptions, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
let b = JSON.parse(body);
|
||||
expect(typeof b.updatedAt).toEqual('string');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('android login providing empty authData block works', (done) => {
|
||||
let headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
};
|
||||
let data = {
|
||||
username: 'pulse1989',
|
||||
password: 'password1234',
|
||||
authData: {}
|
||||
};
|
||||
let requestOptions = {
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/users',
|
||||
body: JSON.stringify(data)
|
||||
};
|
||||
request.post(requestOptions, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
requestOptions.url = 'http://localhost:8378/1/login';
|
||||
request.get(requestOptions, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
let b = JSON.parse(body);
|
||||
expect(typeof b['sessionToken']).toEqual('string');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
var ParseCloudCodePublisher = require('../src/LiveQuery/ParseCloudCodePublisher').ParseCloudCodePublisher;
|
||||
var Parse = require('parse/node');
|
||||
|
||||
describe('ParseCloudCodePublisher', function() {
|
||||
|
||||
beforeEach(function(done) {
|
||||
// Mock ParsePubSub
|
||||
var mockParsePubSub = {
|
||||
createPublisher: jasmine.createSpy('publish').and.returnValue({
|
||||
publish: jasmine.createSpy('publish'),
|
||||
on: jasmine.createSpy('on')
|
||||
}),
|
||||
createSubscriber: jasmine.createSpy('publish').and.returnValue({
|
||||
subscribe: jasmine.createSpy('subscribe'),
|
||||
on: jasmine.createSpy('on')
|
||||
})
|
||||
};
|
||||
jasmine.mockLibrary('../src/LiveQuery/ParsePubSub', 'ParsePubSub', mockParsePubSub);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can initialize', function() {
|
||||
var config = {}
|
||||
var publisher = new ParseCloudCodePublisher(config);
|
||||
|
||||
var ParsePubSub = require('../src/LiveQuery/ParsePubSub').ParsePubSub;
|
||||
expect(ParsePubSub.createPublisher).toHaveBeenCalledWith(config);
|
||||
});
|
||||
|
||||
it('can handle cloud code afterSave request', function() {
|
||||
var publisher = new ParseCloudCodePublisher({});
|
||||
publisher._onCloudCodeMessage = jasmine.createSpy('onCloudCodeMessage');
|
||||
var request = {};
|
||||
publisher.onCloudCodeAfterSave(request);
|
||||
|
||||
expect(publisher._onCloudCodeMessage).toHaveBeenCalledWith('afterSave', request);
|
||||
});
|
||||
|
||||
it('can handle cloud code afterDelete request', function() {
|
||||
var publisher = new ParseCloudCodePublisher({});
|
||||
publisher._onCloudCodeMessage = jasmine.createSpy('onCloudCodeMessage');
|
||||
var request = {};
|
||||
publisher.onCloudCodeAfterDelete(request);
|
||||
|
||||
expect(publisher._onCloudCodeMessage).toHaveBeenCalledWith('afterDelete', request);
|
||||
});
|
||||
|
||||
it('can handle cloud code request', function() {
|
||||
var publisher = new ParseCloudCodePublisher({});
|
||||
var currentParseObject = new Parse.Object('Test');
|
||||
currentParseObject.set('key', 'value');
|
||||
var originalParseObject = new Parse.Object('Test');
|
||||
originalParseObject.set('key', 'originalValue');
|
||||
var request = {
|
||||
object: currentParseObject,
|
||||
original: originalParseObject
|
||||
};
|
||||
publisher._onCloudCodeMessage('afterSave', request);
|
||||
|
||||
var args = publisher.parsePublisher.publish.calls.mostRecent().args;
|
||||
expect(args[0]).toBe('afterSave');
|
||||
var message = JSON.parse(args[1]);
|
||||
expect(message.currentParseObject).toEqual(request.object._toFullJSON());
|
||||
expect(message.originalParseObject).toEqual(request.original._toFullJSON());
|
||||
});
|
||||
|
||||
afterEach(function(){
|
||||
jasmine.restoreLibrary('../src/LiveQuery/ParsePubSub', 'ParsePubSub');
|
||||
});
|
||||
});
|
||||
+60
-19
@@ -1,6 +1,8 @@
|
||||
// This is a port of the test suite:
|
||||
// hungry/js/test/parse_file_test.js
|
||||
|
||||
"use strict";
|
||||
|
||||
var request = require('request');
|
||||
|
||||
var str = "Hello World!";
|
||||
@@ -10,25 +12,49 @@ for (var i = 0; i < str.length; i++) {
|
||||
}
|
||||
|
||||
describe('Parse.File testing', () => {
|
||||
it('works with REST API', done => {
|
||||
var headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
};
|
||||
request.post({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/file.txt',
|
||||
body: 'argle bargle',
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(b.name).toMatch(/_file.txt$/);
|
||||
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.txt$/);
|
||||
request.get(b.url, (error, response, body) => {
|
||||
describe('creating files', () => {
|
||||
it('works with Content-Type', done => {
|
||||
var headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
};
|
||||
request.post({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/file.txt',
|
||||
body: 'argle bargle',
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
expect(body).toEqual('argle bargle');
|
||||
done();
|
||||
var b = JSON.parse(body);
|
||||
expect(b.name).toMatch(/_file.txt$/);
|
||||
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.txt$/);
|
||||
request.get(b.url, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
expect(body).toEqual('argle bargle');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('works without Content-Type', done => {
|
||||
var headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
};
|
||||
request.post({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/file.txt',
|
||||
body: 'argle bargle',
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(b.name).toMatch(/_file.txt$/);
|
||||
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.txt$/);
|
||||
request.get(b.url, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
expect(body).toEqual('argle bargle');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -458,7 +484,22 @@ describe('Parse.File testing', () => {
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('supports files in objects without urls', done => {
|
||||
var file = {
|
||||
__type: 'File',
|
||||
name: '123.txt'
|
||||
};
|
||||
var obj = new Parse.Object('FileTest');
|
||||
obj.set('file', file);
|
||||
obj.save().then(() => {
|
||||
var query = new Parse.Query('FileTest');
|
||||
return query.first();
|
||||
}).then(result => {
|
||||
let fileAgain = result.get('file');
|
||||
expect(fileAgain.url()).toMatch(/123.txt$/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
var request = require('request');
|
||||
var Parse = require('parse/node').Parse;
|
||||
var DatabaseAdapter = require('../src/DatabaseAdapter');
|
||||
|
||||
var database = DatabaseAdapter.getDatabaseConnection('test');
|
||||
let Config = require('../src/Config');
|
||||
|
||||
describe('a GlobalConfig', () => {
|
||||
beforeEach(function(done) {
|
||||
database.rawCollection('_GlobalConfig')
|
||||
.then(coll => coll.updateOne({ '_id': 1}, { $set: { params: { companies: ['US', 'DK'] } } }, { upsert: true }))
|
||||
.then(done());
|
||||
beforeEach(done => {
|
||||
let config = new Config('test');
|
||||
config.database.adaptiveCollection('_GlobalConfig')
|
||||
.then(coll => coll.upsertOne({ '_id': 1 }, { $set: { params: { companies: ['US', 'DK'] } } }))
|
||||
.then(() => { done(); });
|
||||
});
|
||||
|
||||
it('can be retrieved', (done) => {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/config',
|
||||
json: true,
|
||||
url : 'http://localhost:8378/1/config',
|
||||
json : true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
'X-Parse-Master-Key' : 'test'
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(body.params.companies).toEqual(['US', 'DK']);
|
||||
@@ -29,13 +29,13 @@ describe('a GlobalConfig', () => {
|
||||
|
||||
it('can be updated when a master key exists', (done) => {
|
||||
request.put({
|
||||
url: 'http://localhost:8378/1/config',
|
||||
json: true,
|
||||
body: { params: { companies: ['US', 'DK', 'SE'] } },
|
||||
url : 'http://localhost:8378/1/config',
|
||||
json : true,
|
||||
body : { params: { companies: ['US', 'DK', 'SE'] } },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test'
|
||||
},
|
||||
'X-Parse-Master-Key' : 'test'
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(body.result).toEqual(true);
|
||||
@@ -45,34 +45,35 @@ describe('a GlobalConfig', () => {
|
||||
|
||||
it('fail to update if master key is missing', (done) => {
|
||||
request.put({
|
||||
url: 'http://localhost:8378/1/config',
|
||||
json: true,
|
||||
body: { params: { companies: [] } },
|
||||
url : 'http://localhost:8378/1/config',
|
||||
json : true,
|
||||
body : { params: { companies: [] } },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
},
|
||||
'X-Parse-REST-API-Key' : 'rest'
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(401);
|
||||
expect(body.error).toEqual('unauthorized');
|
||||
expect(response.statusCode).toEqual(403);
|
||||
expect(body.error).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('failed getting config when it is missing', (done) => {
|
||||
database.rawCollection('_GlobalConfig')
|
||||
.then(coll => coll.deleteOne({ '_id': 1}, {}, {}))
|
||||
.then(_ => {
|
||||
let config = new Config('test');
|
||||
config.database.adaptiveCollection('_GlobalConfig')
|
||||
.then(coll => coll.deleteOne({ '_id': 1 }))
|
||||
.then(() => {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/config',
|
||||
json: true,
|
||||
url : 'http://localhost:8378/1/config',
|
||||
json : true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
'X-Parse-Master-Key' : 'test'
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(404);
|
||||
expect(body.code).toEqual(Parse.Error.INVALID_KEY_NAME);
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(body.params).toEqual({});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
/* global describe, it, expect, fail, Parse */
|
||||
var request = require('request');
|
||||
var triggers = require('../src/triggers');
|
||||
var HooksController = require('../src/Controllers/HooksController').default;
|
||||
var express = require("express");
|
||||
var bodyParser = require('body-parser');
|
||||
// Inject the hooks API
|
||||
Parse.Hooks = require("../src/cloud-code/Parse.Hooks");
|
||||
|
||||
var port = 12345;
|
||||
var hookServerURL = "http://localhost:"+port;
|
||||
|
||||
var app = express();
|
||||
app.use(bodyParser.json({ 'type': '*/*' }))
|
||||
app.listen(12345);
|
||||
|
||||
|
||||
describe('Hooks', () => {
|
||||
|
||||
it("should have some hooks registered", (done) => {
|
||||
Parse.Hooks.getFunctions().then((res) => {
|
||||
expect(res.constructor).toBe(Array.prototype.constructor);
|
||||
done();
|
||||
}, (err) => {
|
||||
fail(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should have some triggers registered", (done) => {
|
||||
Parse.Hooks.getTriggers().then( (res) => {
|
||||
expect(res.constructor).toBe(Array.prototype.constructor);
|
||||
done();
|
||||
}, (err) => {
|
||||
fail(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should CRUD a function registration", (done) => {
|
||||
// Create
|
||||
Parse.Hooks.createFunction("My-Test-Function", "http://someurl").then((res) => {
|
||||
expect(res.functionName).toBe("My-Test-Function");
|
||||
expect(res.url).toBe("http://someurl")
|
||||
// Find
|
||||
return Parse.Hooks.getFunction("My-Test-Function");
|
||||
}, (err) => {
|
||||
fail(err);
|
||||
done();
|
||||
}).then((res) => {
|
||||
expect(res).not.toBe(null);
|
||||
expect(res).not.toBe(undefined);
|
||||
expect(res.url).toBe("http://someurl");
|
||||
// delete
|
||||
return Parse.Hooks.updateFunction("My-Test-Function", "http://anotherurl");
|
||||
}, (err) => {
|
||||
fail(err);
|
||||
done();
|
||||
}).then((res) => {
|
||||
expect(res.functionName).toBe("My-Test-Function");
|
||||
expect(res.url).toBe("http://anotherurl")
|
||||
|
||||
return Parse.Hooks.deleteFunction("My-Test-Function");
|
||||
}, (err) => {
|
||||
fail(err);
|
||||
done();
|
||||
}).then((res) => {
|
||||
// Find again! but should be deleted
|
||||
return Parse.Hooks.getFunction("My-Test-Function");
|
||||
}, (err) => {
|
||||
fail(err);
|
||||
done();
|
||||
}).then((res) => {
|
||||
fail("Should not succeed")
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err).not.toBe(null);
|
||||
expect(err).not.toBe(undefined);
|
||||
expect(err.code).toBe(143);
|
||||
expect(err.error).toBe("no function named: My-Test-Function is defined")
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should CRUD a trigger registration", (done) => {
|
||||
// Create
|
||||
Parse.Hooks.createTrigger("MyClass","beforeDelete", "http://someurl").then((res) => {
|
||||
expect(res.className).toBe("MyClass");
|
||||
expect(res.triggerName).toBe("beforeDelete");
|
||||
expect(res.url).toBe("http://someurl")
|
||||
// Find
|
||||
return Parse.Hooks.getTrigger("MyClass","beforeDelete");
|
||||
}, (err) => {
|
||||
fail(err);
|
||||
done();
|
||||
}).then((res) => {
|
||||
expect(res).not.toBe(null);
|
||||
expect(res).not.toBe(undefined);
|
||||
expect(res.url).toBe("http://someurl");
|
||||
// delete
|
||||
return Parse.Hooks.updateTrigger("MyClass","beforeDelete", "http://anotherurl");
|
||||
}, (err) => {
|
||||
fail(err);
|
||||
done();
|
||||
}).then((res) => {
|
||||
expect(res.className).toBe("MyClass");
|
||||
expect(res.url).toBe("http://anotherurl")
|
||||
|
||||
return Parse.Hooks.deleteTrigger("MyClass","beforeDelete");
|
||||
}, (err) => {
|
||||
fail(err);
|
||||
done();
|
||||
}).then((res) => {
|
||||
// Find again! but should be deleted
|
||||
return Parse.Hooks.getTrigger("MyClass","beforeDelete");
|
||||
}, (err) => {
|
||||
fail(err);
|
||||
done();
|
||||
}).then(function(){
|
||||
fail("should not succeed");
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err).not.toBe(null);
|
||||
expect(err).not.toBe(undefined);
|
||||
expect(err.code).toBe(143);
|
||||
expect(err.error).toBe("class MyClass does not exist")
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail to register hooks without Master Key", (done) => {
|
||||
request.post(Parse.serverURL+"/hooks/functions", {
|
||||
headers: {
|
||||
"X-Parse-Application-Id": Parse.applicationId,
|
||||
"X-Parse-REST-API-Key": Parse.restKey,
|
||||
},
|
||||
body: JSON.stringify({ url: "http://hello.word", functionName: "SomeFunction"})
|
||||
}, (err, res, body) => {
|
||||
body = JSON.parse(body);
|
||||
expect(body.error).toBe("unauthorized");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should fail trying to create two times the same function", (done) => {
|
||||
Parse.Hooks.createFunction("my_new_function", "http://url.com").then( () => {
|
||||
return Parse.Hooks.createFunction("my_new_function", "http://url.com")
|
||||
}, () => {
|
||||
fail("should create a new function");
|
||||
}).then( () => {
|
||||
fail("should not be able to create the same function");
|
||||
}, (err) => {
|
||||
expect(err).not.toBe(undefined);
|
||||
expect(err).not.toBe(null);
|
||||
expect(err.code).toBe(143);
|
||||
expect(err.error).toBe('function name: my_new_function already exits')
|
||||
return Parse.Hooks.deleteFunction("my_new_function");
|
||||
}).then(() => {
|
||||
done();
|
||||
}, (err) => {
|
||||
fail(err);
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should fail trying to create two times the same trigger", (done) => {
|
||||
Parse.Hooks.createTrigger("MyClass", "beforeSave", "http://url.com").then( () => {
|
||||
return Parse.Hooks.createTrigger("MyClass", "beforeSave", "http://url.com")
|
||||
}, () => {
|
||||
fail("should create a new trigger");
|
||||
}).then( () => {
|
||||
fail("should not be able to create the same trigger");
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(143);
|
||||
expect(err.error).toBe('class MyClass already has trigger beforeSave')
|
||||
return Parse.Hooks.deleteTrigger("MyClass", "beforeSave");
|
||||
}).then(() => {
|
||||
done();
|
||||
}, (err) => {
|
||||
fail(err);
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should fail trying to update a function that don't exist", (done) => {
|
||||
Parse.Hooks.updateFunction("A_COOL_FUNCTION", "http://url.com").then( () => {
|
||||
fail("Should not succeed")
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(143);
|
||||
expect(err.error).toBe('no function named: A_COOL_FUNCTION is defined');
|
||||
return Parse.Hooks.getFunction("A_COOL_FUNCTION")
|
||||
}).then( (res) => {
|
||||
fail("the function should not exist");
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(143);
|
||||
expect(err.error).toBe('no function named: A_COOL_FUNCTION is defined');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail trying to update a trigger that don't exist", (done) => {
|
||||
Parse.Hooks.updateTrigger("AClassName","beforeSave", "http://url.com").then( () => {
|
||||
fail("Should not succeed")
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(143);
|
||||
expect(err.error).toBe('class AClassName does not exist');
|
||||
return Parse.Hooks.getTrigger("AClassName","beforeSave")
|
||||
}).then( (res) => {
|
||||
fail("the function should not exist");
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(143);
|
||||
expect(err.error).toBe('class AClassName does not exist');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should fail trying to create a malformed function", (done) => {
|
||||
Parse.Hooks.createFunction("MyFunction").then( (res) => {
|
||||
fail(res);
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(143);
|
||||
expect(err.error).toBe("invalid hook declaration");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail trying to create a malformed function (REST)", (done) => {
|
||||
request.post(Parse.serverURL+"/hooks/functions", {
|
||||
headers: {
|
||||
"X-Parse-Application-Id": Parse.applicationId,
|
||||
"X-Parse-Master-Key": Parse.masterKey,
|
||||
},
|
||||
body: JSON.stringify({ functionName: "SomeFunction"})
|
||||
}, (err, res, body) => {
|
||||
body = JSON.parse(body);
|
||||
expect(body.error).toBe("invalid hook declaration");
|
||||
expect(body.code).toBe(143);
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
it("should create hooks and properly preload them", (done) => {
|
||||
|
||||
var promises = [];
|
||||
for (var i = 0; i<5; i++) {
|
||||
promises.push(Parse.Hooks.createTrigger("MyClass"+i, "beforeSave", "http://url.com/beforeSave/"+i));
|
||||
promises.push(Parse.Hooks.createFunction("AFunction"+i, "http://url.com/function"+i));
|
||||
}
|
||||
|
||||
Parse.Promise.when(promises).then(function(results){
|
||||
for (var i=0; i<5; i++) {
|
||||
// Delete everything from memory, as the server just started
|
||||
triggers.removeTrigger("beforeSave", "MyClass"+i, Parse.applicationId);
|
||||
triggers.removeFunction("AFunction"+i, Parse.applicationId);
|
||||
expect(triggers.getTrigger("MyClass"+i, "beforeSave", Parse.applicationId)).toBeUndefined();
|
||||
expect(triggers.getFunction("AFunction"+i, Parse.applicationId)).toBeUndefined();
|
||||
}
|
||||
const hooksController = new HooksController(Parse.applicationId);
|
||||
return hooksController.load()
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail();
|
||||
done();
|
||||
}).then(function() {
|
||||
for (var i=0; i<5; i++) {
|
||||
expect(triggers.getTrigger("MyClass"+i, "beforeSave", Parse.applicationId)).not.toBeUndefined();
|
||||
expect(triggers.getFunction("AFunction"+i, Parse.applicationId)).not.toBeUndefined();
|
||||
}
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail();
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should run the function on the test server", (done) => {
|
||||
|
||||
app.post("/SomeFunction", function(req, res) {
|
||||
res.json({success:"OK!"});
|
||||
});
|
||||
|
||||
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/SomeFunction").then(function(){
|
||||
return Parse.Cloud.run("SOME_TEST_FUNCTION")
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("Should not fail creating a function");
|
||||
done();
|
||||
}).then(function(res){
|
||||
expect(res).toBe("OK!");
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("Should not fail calling a function");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should run the function on the test server", (done) => {
|
||||
|
||||
app.post("/SomeFunctionError", function(req, res) {
|
||||
res.json({error: {code: 1337, error: "hacking that one!"}});
|
||||
});
|
||||
// The function is delete as the DB is dropped between calls
|
||||
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/SomeFunctionError").then(function(){
|
||||
return Parse.Cloud.run("SOME_TEST_FUNCTION")
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("Should not fail creating a function");
|
||||
done();
|
||||
}).then(function(res){
|
||||
fail("Should not succeed calling that function");
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(141);
|
||||
expect(err.message.code).toEqual(1337)
|
||||
expect(err.message.error).toEqual("hacking that one!");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should run the beforeSave hook on the test server", (done) => {
|
||||
var triggerCount = 0;
|
||||
app.post("/BeforeSaveSome", function(req, res) {
|
||||
triggerCount++;
|
||||
var object = req.body.object;
|
||||
object.hello = "world";
|
||||
// Would need parse cloud express to set much more
|
||||
// But this should override the key upon return
|
||||
res.json({success: {object: object}});
|
||||
});
|
||||
// The function is delete as the DB is dropped between calls
|
||||
Parse.Hooks.createTrigger("SomeRandomObject", "beforeSave" ,hookServerURL+"/BeforeSaveSome").then(function(){
|
||||
const obj = new Parse.Object("SomeRandomObject");
|
||||
return obj.save();
|
||||
}).then(function(res){
|
||||
expect(triggerCount).toBe(1);
|
||||
return res.fetch();
|
||||
}).then(function(res){
|
||||
expect(res.get("hello")).toEqual("world");
|
||||
done();
|
||||
}).fail((err) => {
|
||||
console.error(err);
|
||||
fail("Should not fail creating a function");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should run the afterSave hook on the test server", (done) => {
|
||||
var triggerCount = 0;
|
||||
var newObjectId;
|
||||
app.post("/AfterSaveSome", function(req, res) {
|
||||
triggerCount++;
|
||||
var obj = new Parse.Object("AnotherObject");
|
||||
obj.set("foo", "bar");
|
||||
obj.save().then(function(obj){
|
||||
newObjectId = obj.id;
|
||||
res.json({success: {}});
|
||||
})
|
||||
});
|
||||
// The function is delete as the DB is dropped between calls
|
||||
Parse.Hooks.createTrigger("SomeRandomObject", "afterSave" ,hookServerURL+"/AfterSaveSome").then(function(){
|
||||
const obj = new Parse.Object("SomeRandomObject");
|
||||
return obj.save();
|
||||
}).then(function(res){
|
||||
var promise = new Parse.Promise();
|
||||
// Wait a bit here as it's an after save
|
||||
setTimeout(function(){
|
||||
expect(triggerCount).toBe(1);
|
||||
var q = new Parse.Query("AnotherObject");
|
||||
q.get(newObjectId).then(function(r){
|
||||
promise.resolve(r);
|
||||
});
|
||||
}, 300)
|
||||
return promise;
|
||||
}).then(function(res){
|
||||
expect(res.get("foo")).toEqual("bar");
|
||||
done();
|
||||
}).fail((err) => {
|
||||
console.error(err);
|
||||
fail("Should not fail creating a function");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
'use strict';
|
||||
// These tests check the Installations functionality of the REST API.
|
||||
// Ported from installation_collection_test.go
|
||||
|
||||
@@ -9,7 +10,7 @@ var Parse = require('parse/node').Parse;
|
||||
var rest = require('../src/rest');
|
||||
|
||||
var config = new Config('test');
|
||||
var database = DatabaseAdapter.getDatabaseConnection('test');
|
||||
let database = DatabaseAdapter.getDatabaseConnection('test', 'test_');
|
||||
|
||||
describe('Installations', () => {
|
||||
|
||||
@@ -118,6 +119,50 @@ describe('Installations', () => {
|
||||
}).catch((error) => { console.log(error); });
|
||||
});
|
||||
|
||||
it('should properly fail queying installations', (done) => {
|
||||
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
var device = 'android';
|
||||
var input = {
|
||||
'installationId': installId,
|
||||
'deviceType': device
|
||||
};
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
let query = new Parse.Query(Parse.Installation);
|
||||
return query.find()
|
||||
}).then((results) => {
|
||||
fail('Should not succeed!');
|
||||
done();
|
||||
}).catch((error) => {
|
||||
expect(error.code).toBe(119);
|
||||
expect(error.message).toBe('Clients aren\'t allowed to perform the find operation on the installation collection.')
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly queying installations with masterKey', (done) => {
|
||||
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
var device = 'android';
|
||||
var input = {
|
||||
'installationId': installId,
|
||||
'deviceType': device
|
||||
};
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
let query = new Parse.Query(Parse.Installation);
|
||||
return query.find({useMasterKey: true});
|
||||
}).then((results) => {
|
||||
expect(results.length).toEqual(1);
|
||||
var obj = results[0].toJSON();
|
||||
expect(obj.installationId).toEqual(installId);
|
||||
expect(obj.deviceType).toEqual(device);
|
||||
done();
|
||||
}).catch((error) => {
|
||||
fail('Should not fail');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with missing ids', (done) => {
|
||||
var input = {
|
||||
'deviceType': 'android',
|
||||
@@ -445,6 +490,52 @@ describe('Installations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('update android device token with duplicate device token', (done) => {
|
||||
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
|
||||
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
|
||||
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||
var input = {
|
||||
'installationId': installId1,
|
||||
'deviceToken': t,
|
||||
'deviceType': 'android'
|
||||
};
|
||||
var firstObject;
|
||||
var secondObject;
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
input = {
|
||||
'installationId': installId2,
|
||||
'deviceType': 'android'
|
||||
};
|
||||
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation',
|
||||
{installationId: installId1}, {});
|
||||
}).then((results) => {
|
||||
expect(results.length).toEqual(1);
|
||||
firstObject = results[0];
|
||||
return database.mongoFind('_Installation',
|
||||
{installationId: installId2}, {});
|
||||
}).then((results) => {
|
||||
expect(results.length).toEqual(1);
|
||||
secondObject = results[0];
|
||||
// Update second installation to conflict with first installation
|
||||
input = {
|
||||
'objectId': secondObject._id,
|
||||
'deviceToken': t
|
||||
};
|
||||
return rest.update(config, auth.nobody(config), '_Installation',
|
||||
secondObject._id, input);
|
||||
}).then(() => {
|
||||
// The first object should have been deleted
|
||||
return database.mongoFind('_Installation', {_id: firstObject._id}, {});
|
||||
}).then((results) => {
|
||||
expect(results.length).toEqual(0);
|
||||
done();
|
||||
}).catch((error) => { console.log(error); });
|
||||
});
|
||||
|
||||
|
||||
it('update ios device token with duplicate device token', (done) => {
|
||||
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
|
||||
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
|
||||
|
||||
@@ -0,0 +1,962 @@
|
||||
var Parse = require('parse/node');
|
||||
var ParseLiveQueryServer = require('../src/LiveQuery/ParseLiveQueryServer').ParseLiveQueryServer;
|
||||
|
||||
// Global mock info
|
||||
var queryHashValue = 'hash';
|
||||
var testUserId = 'userId';
|
||||
var testClassName = 'TestObject';
|
||||
|
||||
describe('ParseLiveQueryServer', function() {
|
||||
|
||||
beforeEach(function(done) {
|
||||
// Mock ParseWebSocketServer
|
||||
var mockParseWebSocketServer = jasmine.createSpy('ParseWebSocketServer');
|
||||
jasmine.mockLibrary('../src/LiveQuery/ParseWebSocketServer', 'ParseWebSocketServer', mockParseWebSocketServer);
|
||||
// Mock Client
|
||||
var mockClient = function() {
|
||||
this.pushConnect = jasmine.createSpy('pushConnect');
|
||||
this.pushSubscribe = jasmine.createSpy('pushSubscribe');
|
||||
this.pushUnsubscribe = jasmine.createSpy('pushUnsubscribe');
|
||||
this.pushDelete = jasmine.createSpy('pushDelete');
|
||||
this.pushCreate = jasmine.createSpy('pushCreate');
|
||||
this.pushEnter = jasmine.createSpy('pushEnter');
|
||||
this.pushUpdate = jasmine.createSpy('pushUpdate');
|
||||
this.pushLeave = jasmine.createSpy('pushLeave');
|
||||
this.addSubscriptionInfo = jasmine.createSpy('addSubscriptionInfo');
|
||||
this.getSubscriptionInfo = jasmine.createSpy('getSubscriptionInfo');
|
||||
this.deleteSubscriptionInfo = jasmine.createSpy('deleteSubscriptionInfo');
|
||||
}
|
||||
mockClient.pushError = jasmine.createSpy('pushError');
|
||||
jasmine.mockLibrary('../src/LiveQuery/Client', 'Client', mockClient);
|
||||
// Mock Subscription
|
||||
var mockSubscriotion = function() {
|
||||
this.addClientSubscription = jasmine.createSpy('addClientSubscription');
|
||||
this.deleteClientSubscription = jasmine.createSpy('deleteClientSubscription');
|
||||
}
|
||||
jasmine.mockLibrary('../src/LiveQuery/Subscription', 'Subscription', mockSubscriotion);
|
||||
// Mock queryHash
|
||||
var mockQueryHash = jasmine.createSpy('matchesQuery').and.returnValue(queryHashValue);
|
||||
jasmine.mockLibrary('../src/LiveQuery/QueryTools', 'queryHash', mockQueryHash);
|
||||
// Mock matchesQuery
|
||||
var mockMatchesQuery = jasmine.createSpy('matchesQuery').and.returnValue(true);
|
||||
jasmine.mockLibrary('../src/LiveQuery/QueryTools', 'matchesQuery', mockMatchesQuery);
|
||||
// Mock tv4
|
||||
var mockValidate = function() {
|
||||
return true;
|
||||
}
|
||||
jasmine.mockLibrary('tv4', 'validate', mockValidate);
|
||||
// Mock ParsePubSub
|
||||
var mockParsePubSub = {
|
||||
createPublisher: function() {
|
||||
return {
|
||||
publish: jasmine.createSpy('publish'),
|
||||
on: jasmine.createSpy('on')
|
||||
}
|
||||
},
|
||||
createSubscriber: function() {
|
||||
return {
|
||||
subscribe: jasmine.createSpy('subscribe'),
|
||||
on: jasmine.createSpy('on')
|
||||
}
|
||||
}
|
||||
};
|
||||
jasmine.mockLibrary('../src/LiveQuery/ParsePubSub', 'ParsePubSub', mockParsePubSub);
|
||||
// Make mock SessionTokenCache
|
||||
var mockSessionTokenCache = function(){
|
||||
this.getUserId = function(sessionToken){
|
||||
if (typeof sessionToken === 'undefined') {
|
||||
return Parse.Promise.as(undefined);
|
||||
}
|
||||
if (sessionToken === null) {
|
||||
return Parse.Promise.error();
|
||||
}
|
||||
return Parse.Promise.as(testUserId);
|
||||
};
|
||||
};
|
||||
jasmine.mockLibrary('../src/LiveQuery/SessionTokenCache', 'SessionTokenCache', mockSessionTokenCache);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can be initialized', function() {
|
||||
var httpServer = {};
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, httpServer);
|
||||
|
||||
expect(parseLiveQueryServer.clientId).toBe(0);
|
||||
expect(parseLiveQueryServer.clients.size).toBe(0);
|
||||
expect(parseLiveQueryServer.subscriptions.size).toBe(0);
|
||||
});
|
||||
|
||||
it('can handle connect command', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var parseWebSocket = {
|
||||
clientId: -1
|
||||
};
|
||||
parseLiveQueryServer._validateKeys = jasmine.createSpy('validateKeys').and.returnValue(true);
|
||||
parseLiveQueryServer._handleConnect(parseWebSocket);
|
||||
|
||||
expect(parseLiveQueryServer.clientId).toBe(1);
|
||||
expect(parseWebSocket.clientId).toBe(0);
|
||||
var client = parseLiveQueryServer.clients.get(0);
|
||||
expect(client).not.toBeNull();
|
||||
// Make sure we send connect response to the client
|
||||
expect(client.pushConnect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can handle subscribe command without clientId', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var incompleteParseConn = {
|
||||
};
|
||||
parseLiveQueryServer._handleSubscribe(incompleteParseConn, {});
|
||||
|
||||
var Client = require('../src/LiveQuery/Client').Client;
|
||||
expect(Client.pushError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can handle subscribe command with new query', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Add mock client
|
||||
var clientId = 1;
|
||||
var client = addMockClient(parseLiveQueryServer, clientId);
|
||||
// Handle mock subscription
|
||||
var parseWebSocket = {
|
||||
clientId: clientId
|
||||
};
|
||||
var query = {
|
||||
className: 'test',
|
||||
where: {
|
||||
key: 'value'
|
||||
},
|
||||
fields: [ 'test' ]
|
||||
}
|
||||
var requestId = 2;
|
||||
var request = {
|
||||
query: query,
|
||||
requestId: requestId,
|
||||
sessionToken: 'sessionToken'
|
||||
}
|
||||
parseLiveQueryServer._handleSubscribe(parseWebSocket, request);
|
||||
|
||||
// Make sure we add the subscription to the server
|
||||
var subscriptions = parseLiveQueryServer.subscriptions;
|
||||
expect(subscriptions.size).toBe(1);
|
||||
expect(subscriptions.get(query.className)).not.toBeNull();
|
||||
var classSubscriptions = subscriptions.get(query.className);
|
||||
expect(classSubscriptions.size).toBe(1);
|
||||
expect(classSubscriptions.get('hash')).not.toBeNull();
|
||||
// TODO(check subscription constructor to verify we pass the right argument)
|
||||
// Make sure we add clientInfo to the subscription
|
||||
var subscription = classSubscriptions.get('hash');
|
||||
expect(subscription.addClientSubscription).toHaveBeenCalledWith(clientId, requestId);
|
||||
// Make sure we add subscriptionInfo to the client
|
||||
var args = client.addSubscriptionInfo.calls.first().args;
|
||||
expect(args[0]).toBe(requestId);
|
||||
expect(args[1].fields).toBe(query.fields);
|
||||
expect(args[1].sessionToken).toBe(request.sessionToken);
|
||||
// Make sure we send subscribe response to the client
|
||||
expect(client.pushSubscribe).toHaveBeenCalledWith(requestId);
|
||||
});
|
||||
|
||||
it('can handle subscribe command with existing query', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Add two mock clients
|
||||
var clientId = 1;
|
||||
var client = addMockClient(parseLiveQueryServer, clientId);
|
||||
var clientIdAgain = 2;
|
||||
var clientAgain = addMockClient(parseLiveQueryServer, clientIdAgain);
|
||||
// Add subscription for mock client 1
|
||||
var parseWebSocket = {
|
||||
clientId: clientId
|
||||
};
|
||||
var requestId = 2;
|
||||
var query = {
|
||||
className: 'test',
|
||||
where: {
|
||||
key: 'value'
|
||||
},
|
||||
fields: [ 'test' ]
|
||||
}
|
||||
addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query);
|
||||
// Add subscription for mock client 2
|
||||
var parseWebSocketAgain = {
|
||||
clientId: clientIdAgain
|
||||
};
|
||||
var queryAgain = {
|
||||
className: 'test',
|
||||
where: {
|
||||
key: 'value'
|
||||
},
|
||||
fields: [ 'testAgain' ]
|
||||
}
|
||||
var requestIdAgain = 1;
|
||||
addMockSubscription(parseLiveQueryServer, clientIdAgain, requestIdAgain, parseWebSocketAgain, queryAgain);
|
||||
|
||||
// Make sure we only have one subscription
|
||||
var subscriptions = parseLiveQueryServer.subscriptions;
|
||||
expect(subscriptions.size).toBe(1);
|
||||
expect(subscriptions.get(query.className)).not.toBeNull();
|
||||
var classSubscriptions = subscriptions.get(query.className);
|
||||
expect(classSubscriptions.size).toBe(1);
|
||||
expect(classSubscriptions.get('hash')).not.toBeNull();
|
||||
// Make sure we add clientInfo to the subscription
|
||||
var subscription = classSubscriptions.get('hash');
|
||||
// Make sure client 2 info has been added
|
||||
var args = subscription.addClientSubscription.calls.mostRecent().args;
|
||||
expect(args).toEqual([clientIdAgain, requestIdAgain]);
|
||||
// Make sure we add subscriptionInfo to the client 2
|
||||
args = clientAgain.addSubscriptionInfo.calls.mostRecent().args;
|
||||
expect(args[0]).toBe(requestIdAgain);
|
||||
expect(args[1].fields).toBe(queryAgain.fields);
|
||||
});
|
||||
|
||||
it('can handle unsubscribe command without clientId', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var incompleteParseConn = {
|
||||
};
|
||||
parseLiveQueryServer._handleUnsubscribe(incompleteParseConn, {});
|
||||
|
||||
var Client = require('../src/LiveQuery/Client').Client;
|
||||
expect(Client.pushError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can handle unsubscribe command without not existed client', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var parseWebSocket = {
|
||||
clientId: 1
|
||||
};
|
||||
parseLiveQueryServer._handleUnsubscribe(parseWebSocket, {});
|
||||
|
||||
var Client = require('../src/LiveQuery/Client').Client;
|
||||
expect(Client.pushError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can handle unsubscribe command without not existed query', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Add mock client
|
||||
var clientId = 1;
|
||||
var client = addMockClient(parseLiveQueryServer, clientId);
|
||||
// Handle unsubscribe command
|
||||
var parseWebSocket = {
|
||||
clientId: 1
|
||||
};
|
||||
parseLiveQueryServer._handleUnsubscribe(parseWebSocket, {});
|
||||
|
||||
var Client = require('../src/LiveQuery/Client').Client;
|
||||
expect(Client.pushError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can handle unsubscribe command', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Add mock client
|
||||
var clientId = 1;
|
||||
var client = addMockClient(parseLiveQueryServer, clientId);
|
||||
// Add subscription for mock client
|
||||
var parseWebSocket = {
|
||||
clientId: 1
|
||||
};
|
||||
var requestId = 2;
|
||||
var subscription = addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket);
|
||||
// Mock client.getSubscriptionInfo
|
||||
var subscriptionInfo = client.addSubscriptionInfo.calls.mostRecent().args[1];
|
||||
client.getSubscriptionInfo = function() {
|
||||
return subscriptionInfo;
|
||||
};
|
||||
// Handle unsubscribe command
|
||||
var requestAgain = {
|
||||
requestId: requestId
|
||||
};
|
||||
parseLiveQueryServer._handleUnsubscribe(parseWebSocket, requestAgain);
|
||||
|
||||
// Make sure we delete subscription from client
|
||||
expect(client.deleteSubscriptionInfo).toHaveBeenCalledWith(requestId);
|
||||
// Make sure we delete client from subscription
|
||||
expect(subscription.deleteClientSubscription).toHaveBeenCalledWith(clientId, requestId);
|
||||
// Make sure we clear subscription in the server
|
||||
var subscriptions = parseLiveQueryServer.subscriptions;
|
||||
expect(subscriptions.size).toBe(0);
|
||||
});
|
||||
|
||||
it('can set connect command message handler for a parseWebSocket', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Register mock connect/subscribe/unsubscribe handler for the server
|
||||
parseLiveQueryServer._handleConnect = jasmine.createSpy('_handleSubscribe');
|
||||
// Make mock parseWebsocket
|
||||
var EventEmitter = require('events');
|
||||
var parseWebSocket = new EventEmitter();
|
||||
// Register message handlers for the parseWebSocket
|
||||
parseLiveQueryServer._onConnect(parseWebSocket);
|
||||
|
||||
// Check connect request
|
||||
var connectRequest = {
|
||||
op: 'connect'
|
||||
};
|
||||
// Trigger message event
|
||||
parseWebSocket.emit('message', connectRequest);
|
||||
// Make sure _handleConnect is called
|
||||
var args = parseLiveQueryServer._handleConnect.calls.mostRecent().args;
|
||||
expect(args[0]).toBe(parseWebSocket);
|
||||
});
|
||||
|
||||
it('can set subscribe command message handler for a parseWebSocket', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Register mock connect/subscribe/unsubscribe handler for the server
|
||||
parseLiveQueryServer._handleSubscribe = jasmine.createSpy('_handleSubscribe');
|
||||
// Make mock parseWebsocket
|
||||
var EventEmitter = require('events');
|
||||
var parseWebSocket = new EventEmitter();
|
||||
// Register message handlers for the parseWebSocket
|
||||
parseLiveQueryServer._onConnect(parseWebSocket);
|
||||
|
||||
// Check subscribe request
|
||||
var subscribeRequest = '{"op":"subscribe"}';
|
||||
// Trigger message event
|
||||
parseWebSocket.emit('message', subscribeRequest);
|
||||
// Make sure _handleSubscribe is called
|
||||
var args = parseLiveQueryServer._handleSubscribe.calls.mostRecent().args;
|
||||
expect(args[0]).toBe(parseWebSocket);
|
||||
expect(JSON.stringify(args[1])).toBe(subscribeRequest);
|
||||
});
|
||||
|
||||
it('can set unsubscribe command message handler for a parseWebSocket', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Register mock connect/subscribe/unsubscribe handler for the server
|
||||
parseLiveQueryServer._handleUnsubscribe = jasmine.createSpy('_handleSubscribe');
|
||||
// Make mock parseWebsocket
|
||||
var EventEmitter = require('events');
|
||||
var parseWebSocket = new EventEmitter();
|
||||
// Register message handlers for the parseWebSocket
|
||||
parseLiveQueryServer._onConnect(parseWebSocket);
|
||||
|
||||
// Check unsubscribe request
|
||||
var unsubscribeRequest = '{"op":"unsubscribe"}';
|
||||
// Trigger message event
|
||||
parseWebSocket.emit('message', unsubscribeRequest);
|
||||
// Make sure _handleUnsubscribe is called
|
||||
var args = parseLiveQueryServer._handleUnsubscribe.calls.mostRecent().args;
|
||||
expect(args[0]).toBe(parseWebSocket);
|
||||
expect(JSON.stringify(args[1])).toBe(unsubscribeRequest);
|
||||
});
|
||||
|
||||
it('can set unknown command message handler for a parseWebSocket', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make mock parseWebsocket
|
||||
var EventEmitter = require('events');
|
||||
var parseWebSocket = new EventEmitter();
|
||||
// Register message handlers for the parseWebSocket
|
||||
parseLiveQueryServer._onConnect(parseWebSocket);
|
||||
|
||||
// Check unknown request
|
||||
var unknownRequest = '{"op":"unknown"}';
|
||||
// Trigger message event
|
||||
parseWebSocket.emit('message', unknownRequest);
|
||||
var Client = require('../src/LiveQuery/Client').Client;
|
||||
expect(Client.pushError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can set disconnect command message handler for a parseWebSocket which has not registered to the server', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var EventEmitter = require('events');
|
||||
var parseWebSocket = new EventEmitter();
|
||||
parseWebSocket.clientId = 1;
|
||||
// Register message handlers for the parseWebSocket
|
||||
parseLiveQueryServer._onConnect(parseWebSocket);
|
||||
|
||||
// Make sure we do not crash
|
||||
// Trigger disconnect event
|
||||
parseWebSocket.emit('disconnect');
|
||||
});
|
||||
|
||||
// TODO: Test server can set disconnect command message handler for a parseWebSocket
|
||||
|
||||
it('has no subscription and can handle object delete command', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make deletedParseObject
|
||||
var parseObject = new Parse.Object(testClassName);
|
||||
parseObject._finishFetch({
|
||||
key: 'value',
|
||||
className: testClassName
|
||||
});
|
||||
// Make mock message
|
||||
var message = {
|
||||
currentParseObject: parseObject
|
||||
};
|
||||
// Make sure we do not crash in this case
|
||||
parseLiveQueryServer._onAfterDelete(message, {});
|
||||
});
|
||||
|
||||
it('can handle object delete command which does not match any subscription', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make deletedParseObject
|
||||
var parseObject = new Parse.Object(testClassName);
|
||||
parseObject._finishFetch({
|
||||
key: 'value',
|
||||
className: testClassName
|
||||
});
|
||||
// Make mock message
|
||||
var message = {
|
||||
currentParseObject: parseObject
|
||||
};
|
||||
|
||||
// Add mock client
|
||||
var clientId = 1;
|
||||
addMockClient(parseLiveQueryServer, clientId);
|
||||
// Add mock subscription
|
||||
var requestId = 2;
|
||||
addMockSubscription(parseLiveQueryServer, clientId, requestId);
|
||||
var client = parseLiveQueryServer.clients.get(clientId);
|
||||
// Mock _matchesSubscription to return not matching
|
||||
parseLiveQueryServer._matchesSubscription = function() {
|
||||
return false;
|
||||
};
|
||||
parseLiveQueryServer._matchesACL = function() {
|
||||
return true;
|
||||
};
|
||||
parseLiveQueryServer._onAfterDelete(message);
|
||||
|
||||
// Make sure we do not send command to client
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can handle object delete command which matches some subscriptions', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make deletedParseObject
|
||||
var parseObject = new Parse.Object(testClassName);
|
||||
parseObject._finishFetch({
|
||||
key: 'value',
|
||||
className: testClassName
|
||||
});
|
||||
// Make mock message
|
||||
var message = {
|
||||
currentParseObject: parseObject
|
||||
};
|
||||
// Add mock client
|
||||
var clientId = 1;
|
||||
addMockClient(parseLiveQueryServer, clientId);
|
||||
// Add mock subscription
|
||||
var requestId = 2;
|
||||
addMockSubscription(parseLiveQueryServer, clientId, requestId);
|
||||
var client = parseLiveQueryServer.clients.get(clientId);
|
||||
// Mock _matchesSubscription to return matching
|
||||
parseLiveQueryServer._matchesSubscription = function() {
|
||||
return true;
|
||||
};
|
||||
parseLiveQueryServer._matchesACL = function() {
|
||||
return Parse.Promise.as(true);
|
||||
};
|
||||
parseLiveQueryServer._onAfterDelete(message);
|
||||
|
||||
// Make sure we send command to client, since _matchesACL is async, we have to
|
||||
// wait and check
|
||||
setTimeout(function() {
|
||||
expect(client.pushDelete).toHaveBeenCalled();
|
||||
done();
|
||||
}, jasmine.ASYNC_TEST_WAIT_TIME);
|
||||
});
|
||||
|
||||
it('has no subscription and can handle object save command', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make mock request message
|
||||
var message = generateMockMessage();
|
||||
// Make sure we do not crash in this case
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
});
|
||||
|
||||
it('can handle object save command which does not match any subscription', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make mock request message
|
||||
var message = generateMockMessage();
|
||||
// Add mock client
|
||||
var clientId = 1;
|
||||
var client = addMockClient(parseLiveQueryServer, clientId);
|
||||
// Add mock subscription
|
||||
var requestId = 2;
|
||||
addMockSubscription(parseLiveQueryServer, clientId, requestId);
|
||||
// Mock _matchesSubscription to return not matching
|
||||
parseLiveQueryServer._matchesSubscription = function() {
|
||||
return false;
|
||||
};
|
||||
parseLiveQueryServer._matchesACL = function() {
|
||||
return Parse.Promise.as(true)
|
||||
};
|
||||
// Trigger onAfterSave
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we do not send command to client
|
||||
setTimeout(function(){
|
||||
expect(client.pushCreate).not.toHaveBeenCalled();
|
||||
expect(client.pushEnter).not.toHaveBeenCalled();
|
||||
expect(client.pushUpdate).not.toHaveBeenCalled();
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
expect(client.pushLeave).not.toHaveBeenCalled();
|
||||
done();
|
||||
}, jasmine.ASYNC_TEST_WAIT_TIME);
|
||||
});
|
||||
|
||||
it('can handle object enter command which matches some subscriptions', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make mock request message
|
||||
var message = generateMockMessage(true);
|
||||
// Add mock client
|
||||
var clientId = 1;
|
||||
var client = addMockClient(parseLiveQueryServer, clientId);
|
||||
// Add mock subscription
|
||||
var requestId = 2;
|
||||
addMockSubscription(parseLiveQueryServer, clientId, requestId);
|
||||
// Mock _matchesSubscription to return matching
|
||||
// In order to mimic a enter, we need original match return false
|
||||
// and the current match return true
|
||||
var counter = 0;
|
||||
parseLiveQueryServer._matchesSubscription = function(parseObject, subscription){
|
||||
if (!parseObject) {
|
||||
return false;
|
||||
}
|
||||
counter += 1;
|
||||
return counter % 2 === 0;
|
||||
};
|
||||
parseLiveQueryServer._matchesACL = function() {
|
||||
return Parse.Promise.as(true)
|
||||
};
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we send enter command to client
|
||||
setTimeout(function(){
|
||||
expect(client.pushCreate).not.toHaveBeenCalled();
|
||||
expect(client.pushEnter).toHaveBeenCalled();
|
||||
expect(client.pushUpdate).not.toHaveBeenCalled();
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
expect(client.pushLeave).not.toHaveBeenCalled();
|
||||
done();
|
||||
}, jasmine.ASYNC_TEST_WAIT_TIME);
|
||||
});
|
||||
|
||||
it('can handle object update command which matches some subscriptions', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make mock request message
|
||||
var message = generateMockMessage(true);
|
||||
// Add mock client
|
||||
var clientId = 1;
|
||||
var client = addMockClient(parseLiveQueryServer, clientId);
|
||||
// Add mock subscription
|
||||
var requestId = 2;
|
||||
addMockSubscription(parseLiveQueryServer, clientId, requestId);
|
||||
// Mock _matchesSubscription to return matching
|
||||
parseLiveQueryServer._matchesSubscription = function(parseObject, subscription){
|
||||
if (!parseObject) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
parseLiveQueryServer._matchesACL = function() {
|
||||
return Parse.Promise.as(true)
|
||||
};
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we send update command to client
|
||||
setTimeout(function(){
|
||||
expect(client.pushCreate).not.toHaveBeenCalled();
|
||||
expect(client.pushEnter).not.toHaveBeenCalled();
|
||||
expect(client.pushUpdate).toHaveBeenCalled();
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
expect(client.pushLeave).not.toHaveBeenCalled();
|
||||
done();
|
||||
}, jasmine.ASYNC_TEST_WAIT_TIME);
|
||||
});
|
||||
|
||||
it('can handle object leave command which matches some subscriptions', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make mock request message
|
||||
var message = generateMockMessage(true);
|
||||
// Add mock client
|
||||
var clientId = 1;
|
||||
var client = addMockClient(parseLiveQueryServer, clientId);
|
||||
// Add mock subscription
|
||||
var requestId = 2;
|
||||
addMockSubscription(parseLiveQueryServer, clientId, requestId);
|
||||
// Mock _matchesSubscription to return matching
|
||||
// In order to mimic a leave, we need original match return true
|
||||
// and the current match return false
|
||||
var counter = 0;
|
||||
parseLiveQueryServer._matchesSubscription = function(parseObject, subscription){
|
||||
if (!parseObject) {
|
||||
return false;
|
||||
}
|
||||
counter += 1;
|
||||
return counter % 2 !== 0;
|
||||
};
|
||||
parseLiveQueryServer._matchesACL = function() {
|
||||
return Parse.Promise.as(true)
|
||||
};
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we send leave command to client
|
||||
setTimeout(function(){
|
||||
expect(client.pushCreate).not.toHaveBeenCalled();
|
||||
expect(client.pushEnter).not.toHaveBeenCalled();
|
||||
expect(client.pushUpdate).not.toHaveBeenCalled();
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
expect(client.pushLeave).toHaveBeenCalled();
|
||||
done();
|
||||
}, jasmine.ASYNC_TEST_WAIT_TIME);
|
||||
});
|
||||
|
||||
it('can handle object create command which matches some subscriptions', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make mock request message
|
||||
var message = generateMockMessage();
|
||||
// Add mock client
|
||||
var clientId = 1;
|
||||
var client = addMockClient(parseLiveQueryServer, clientId);
|
||||
// Add mock subscription
|
||||
var requestId = 2;
|
||||
addMockSubscription(parseLiveQueryServer, clientId, requestId);
|
||||
// Mock _matchesSubscription to return matching
|
||||
parseLiveQueryServer._matchesSubscription = function(parseObject, subscription){
|
||||
if (!parseObject) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
parseLiveQueryServer._matchesACL = function() {
|
||||
return Parse.Promise.as(true)
|
||||
};
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we send create command to client
|
||||
setTimeout(function(){
|
||||
expect(client.pushCreate).toHaveBeenCalled();
|
||||
expect(client.pushEnter).not.toHaveBeenCalled();
|
||||
expect(client.pushUpdate).not.toHaveBeenCalled();
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
expect(client.pushLeave).not.toHaveBeenCalled();
|
||||
done();
|
||||
}, jasmine.ASYNC_TEST_WAIT_TIME);
|
||||
});
|
||||
|
||||
it('can match subscription for null or undefined parse object', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make mock subscription
|
||||
var subscription = {
|
||||
match: jasmine.createSpy('match')
|
||||
}
|
||||
|
||||
expect(parseLiveQueryServer._matchesSubscription(null, subscription)).toBe(false);
|
||||
expect(parseLiveQueryServer._matchesSubscription(undefined, subscription)).toBe(false);
|
||||
// Make sure subscription.match is not called
|
||||
expect(subscription.match).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can match subscription', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make mock subscription
|
||||
var subscription = {
|
||||
query: {}
|
||||
}
|
||||
var parseObject = {};
|
||||
expect(parseLiveQueryServer._matchesSubscription(parseObject, subscription)).toBe(true);
|
||||
// Make sure matchesQuery is called
|
||||
var matchesQuery = require('../src/LiveQuery/QueryTools').matchesQuery;
|
||||
expect(matchesQuery).toHaveBeenCalledWith(parseObject, subscription.query);
|
||||
});
|
||||
|
||||
it('can inflate parse object', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make mock request
|
||||
var objectJSON = {
|
||||
"className":"testClassName",
|
||||
"createdAt":"2015-12-22T01:51:12.955Z",
|
||||
"key":"value",
|
||||
"objectId":"BfwxBCz6yW",
|
||||
"updatedAt":"2016-01-05T00:46:45.659Z"
|
||||
};
|
||||
var originalObjectJSON = {
|
||||
"className":"testClassName",
|
||||
"createdAt":"2015-12-22T01:51:12.955Z",
|
||||
"key":"originalValue",
|
||||
"objectId":"BfwxBCz6yW",
|
||||
"updatedAt":"2016-01-05T00:46:45.659Z"
|
||||
};
|
||||
var message = {
|
||||
currentParseObject: objectJSON,
|
||||
originalParseObject: originalObjectJSON
|
||||
};
|
||||
// Inflate the object
|
||||
parseLiveQueryServer._inflateParseObject(message);
|
||||
|
||||
// Verify object
|
||||
var object = message.currentParseObject;
|
||||
expect(object instanceof Parse.Object).toBeTruthy();
|
||||
expect(object.get('key')).toEqual('value');
|
||||
expect(object.className).toEqual('testClassName');
|
||||
expect(object.id).toBe('BfwxBCz6yW');
|
||||
expect(object.createdAt).not.toBeUndefined();
|
||||
expect(object.updatedAt).not.toBeUndefined();
|
||||
// Verify original object
|
||||
var originalObject = message.originalParseObject;
|
||||
expect(originalObject instanceof Parse.Object).toBeTruthy();
|
||||
expect(originalObject.get('key')).toEqual('originalValue');
|
||||
expect(originalObject.className).toEqual('testClassName');
|
||||
expect(originalObject.id).toBe('BfwxBCz6yW');
|
||||
expect(originalObject.createdAt).not.toBeUndefined();
|
||||
expect(originalObject.updatedAt).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('can match undefined ACL', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var client = {};
|
||||
var requestId = 0;
|
||||
|
||||
parseLiveQueryServer._matchesACL(undefined, client, requestId).then(function(isMatched) {
|
||||
expect(isMatched).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can match ACL with none exist requestId', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var acl = new Parse.ACL();
|
||||
var client = {
|
||||
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue(undefined)
|
||||
};
|
||||
var requestId = 0;
|
||||
|
||||
var isChecked = false;
|
||||
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
|
||||
expect(isMatched).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can match ACL with public read access', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
var client = {
|
||||
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
|
||||
sessionToken: 'sessionToken'
|
||||
})
|
||||
};
|
||||
var requestId = 0;
|
||||
|
||||
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
|
||||
expect(isMatched).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can match ACL with valid subscription sessionToken', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var acl = new Parse.ACL();
|
||||
acl.setReadAccess(testUserId, true);
|
||||
var client = {
|
||||
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
|
||||
sessionToken: 'sessionToken'
|
||||
})
|
||||
};
|
||||
var requestId = 0;
|
||||
|
||||
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
|
||||
expect(isMatched).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can match ACL with valid client sessionToken', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var acl = new Parse.ACL();
|
||||
acl.setReadAccess(testUserId, true);
|
||||
// Mock sessionTokenCache will return false when sessionToken is undefined
|
||||
var client = {
|
||||
sessionToken: 'sessionToken',
|
||||
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
|
||||
sessionToken: undefined
|
||||
})
|
||||
};
|
||||
var requestId = 0;
|
||||
|
||||
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
|
||||
expect(isMatched).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can match ACL with invalid subscription and client sessionToken', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var acl = new Parse.ACL();
|
||||
acl.setReadAccess(testUserId, true);
|
||||
// Mock sessionTokenCache will return false when sessionToken is undefined
|
||||
var client = {
|
||||
sessionToken: undefined,
|
||||
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
|
||||
sessionToken: undefined
|
||||
})
|
||||
};
|
||||
var requestId = 0;
|
||||
|
||||
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
|
||||
expect(isMatched).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can match ACL with subscription sessionToken checking error', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var acl = new Parse.ACL();
|
||||
acl.setReadAccess(testUserId, true);
|
||||
// Mock sessionTokenCache will return error when sessionToken is null, this is just
|
||||
// the behaviour of our mock sessionTokenCache, not real sessionTokenCache
|
||||
var client = {
|
||||
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
|
||||
sessionToken: null
|
||||
})
|
||||
};
|
||||
var requestId = 0;
|
||||
|
||||
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
|
||||
expect(isMatched).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can match ACL with client sessionToken checking error', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var acl = new Parse.ACL();
|
||||
acl.setReadAccess(testUserId, true);
|
||||
// Mock sessionTokenCache will return error when sessionToken is null
|
||||
var client = {
|
||||
sessionToken: null,
|
||||
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
|
||||
sessionToken: null
|
||||
})
|
||||
};
|
||||
var requestId = 0;
|
||||
|
||||
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
|
||||
expect(isMatched).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can validate key when valid key is provided', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer({}, {
|
||||
keyPairs: {
|
||||
clientKey: 'test'
|
||||
}
|
||||
});
|
||||
var request = {
|
||||
clientKey: 'test'
|
||||
}
|
||||
|
||||
expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('can validate key when invalid key is provided', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer({}, {
|
||||
keyPairs: {
|
||||
clientKey: 'test'
|
||||
}
|
||||
});
|
||||
var request = {
|
||||
clientKey: 'error'
|
||||
}
|
||||
|
||||
expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).not.toBeTruthy();
|
||||
});
|
||||
|
||||
it('can validate key when key is not provided', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer({}, {
|
||||
keyPairs: {
|
||||
clientKey: 'test'
|
||||
}
|
||||
});
|
||||
var request = {
|
||||
}
|
||||
|
||||
expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).not.toBeTruthy();
|
||||
});
|
||||
|
||||
it('can validate key when validKerPairs is empty', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer({}, {});
|
||||
var request = {
|
||||
}
|
||||
|
||||
expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).toBeTruthy();
|
||||
});
|
||||
|
||||
afterEach(function(){
|
||||
jasmine.restoreLibrary('../src/LiveQuery/ParseWebSocketServer', 'ParseWebSocketServer');
|
||||
jasmine.restoreLibrary('../src/LiveQuery/Client', 'Client');
|
||||
jasmine.restoreLibrary('../src/LiveQuery/Subscription', 'Subscription');
|
||||
jasmine.restoreLibrary('../src/LiveQuery/QueryTools', 'queryHash');
|
||||
jasmine.restoreLibrary('../src/LiveQuery/QueryTools', 'matchesQuery');
|
||||
jasmine.restoreLibrary('tv4', 'validate');
|
||||
jasmine.restoreLibrary('../src/LiveQuery/ParsePubSub', 'ParsePubSub');
|
||||
jasmine.restoreLibrary('../src/LiveQuery/SessionTokenCache', 'SessionTokenCache');
|
||||
});
|
||||
|
||||
// Helper functions to add mock client and subscription to a liveQueryServer
|
||||
function addMockClient(parseLiveQueryServer, clientId) {
|
||||
var Client = require('../src/LiveQuery/Client').Client;
|
||||
var client = new Client(clientId, {});
|
||||
parseLiveQueryServer.clients.set(clientId, client);
|
||||
return client;
|
||||
}
|
||||
|
||||
function addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query) {
|
||||
// If parseWebSocket is null, we use the default one
|
||||
if (!parseWebSocket) {
|
||||
var EventEmitter = require('events');
|
||||
parseWebSocket = new EventEmitter();
|
||||
}
|
||||
parseWebSocket.clientId = clientId;
|
||||
// If query is null, we use the default one
|
||||
if (!query) {
|
||||
query = {
|
||||
className: testClassName,
|
||||
where: {
|
||||
key: 'value'
|
||||
},
|
||||
fields: [ 'test' ]
|
||||
};
|
||||
}
|
||||
var request = {
|
||||
query: query,
|
||||
requestId: requestId,
|
||||
sessionToken: 'sessionToken'
|
||||
};
|
||||
parseLiveQueryServer._handleSubscribe(parseWebSocket, request);
|
||||
|
||||
// Make mock subscription
|
||||
var subscription = parseLiveQueryServer.subscriptions.get(query.className).get(queryHashValue);
|
||||
subscription.hasSubscribingClient = function() {
|
||||
return false;
|
||||
}
|
||||
subscription.className = query.className;
|
||||
subscription.hash = queryHashValue;
|
||||
if (subscription.clientRequestIds && subscription.clientRequestIds.has(clientId)) {
|
||||
subscription.clientRequestIds.get(clientId).push(requestId);
|
||||
} else {
|
||||
subscription.clientRequestIds = new Map([[clientId, [requestId]]]);
|
||||
}
|
||||
return subscription;
|
||||
}
|
||||
|
||||
// Helper functiosn to generate request message
|
||||
function generateMockMessage(hasOriginalParseObject) {
|
||||
var parseObject = new Parse.Object(testClassName);
|
||||
parseObject._finishFetch({
|
||||
key: 'value',
|
||||
className: testClassName
|
||||
});
|
||||
var message = {
|
||||
currentParseObject: parseObject
|
||||
};
|
||||
if (hasOriginalParseObject) {
|
||||
var originalParseObject = new Parse.Object(testClassName);
|
||||
originalParseObject._finishFetch({
|
||||
key: 'originalValue',
|
||||
className: testClassName
|
||||
});
|
||||
message.originalParseObject = originalParseObject;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
"use strict";
|
||||
// This is a port of the test suite:
|
||||
// hungry/js/test/parse_object_test.js
|
||||
//
|
||||
@@ -336,6 +337,34 @@ describe('Parse.Object testing', () => {
|
||||
item.save({ "foo^bar": "baz" }).then(fail, done);
|
||||
});
|
||||
|
||||
it("invalid __type", function(done) {
|
||||
var item = new Parse.Object("Item");
|
||||
var types = ['Pointer', 'File', 'Date', 'GeoPoint', 'Bytes'];
|
||||
var Error = Parse.Error;
|
||||
var tests = types.map(type => {
|
||||
var test = new Parse.Object("Item");
|
||||
test.set('foo', {
|
||||
__type: type
|
||||
});
|
||||
return test;
|
||||
});
|
||||
var next = function(index) {
|
||||
if (index < tests.length) {
|
||||
tests[index].save().then(fail, error => {
|
||||
expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE);
|
||||
next(index + 1);
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
}
|
||||
item.save({
|
||||
"foo": {
|
||||
__type: "IvalidName"
|
||||
}
|
||||
}).then(fail, err => next(0));
|
||||
});
|
||||
|
||||
it("simple field deletion", function(done) {
|
||||
var simple = new Parse.Object("SimpleObject");
|
||||
simple.save({
|
||||
@@ -571,6 +600,49 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("addUnique with object", function(done) {
|
||||
var x1 = new Parse.Object('X');
|
||||
x1.set('stuff', [ 1, {'hello': 'world'}, {'foo': 'bar'}]);
|
||||
x1.save().then(() => {
|
||||
var objectId = x1.id;
|
||||
var x2 = new Parse.Object('X', {objectId: objectId});
|
||||
x2.addUnique('stuff', {'hello': 'world'});
|
||||
x2.addUnique('stuff', {'bar': 'baz'});
|
||||
expect(x2.get('stuff')).toEqual([{'hello': 'world'}, {'bar': 'baz'}]);
|
||||
return x2.save();
|
||||
}).then(() => {
|
||||
var query = new Parse.Query('X');
|
||||
return query.get(x1.id);
|
||||
}).then((x3) => {
|
||||
expect(x3.get('stuff')).toEqual([1, {'hello': 'world'}, {'foo': 'bar'}, {'bar': 'baz'}]);
|
||||
done();
|
||||
}, (error) => {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("removes with object", function(done) {
|
||||
var x1 = new Parse.Object('X');
|
||||
x1.set('stuff', [ 1, {'hello': 'world'}, {'foo': 'bar'}]);
|
||||
x1.save().then(() => {
|
||||
var objectId = x1.id;
|
||||
var x2 = new Parse.Object('X', {objectId: objectId});
|
||||
x2.remove('stuff', {'hello': 'world'});
|
||||
expect(x2.get('stuff')).toEqual([]);
|
||||
return x2.save();
|
||||
}).then(() => {
|
||||
var query = new Parse.Query('X');
|
||||
return query.get(x1.id);
|
||||
}).then((x3) => {
|
||||
expect(x3.get('stuff')).toEqual([1, {'foo': 'bar'}]);
|
||||
done();
|
||||
}, (error) => {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("dirty attributes", function(done) {
|
||||
var object = new Parse.Object("TestObject");
|
||||
object.set("cat", "good");
|
||||
@@ -1736,4 +1808,82 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should create nested keys with _", done => {
|
||||
const object = new Parse.Object("AnObject");
|
||||
object.set("foo", {
|
||||
"_bar": "_",
|
||||
"baz_bar": 1,
|
||||
"__foo_bar": true,
|
||||
"_0": "underscore_zero",
|
||||
"_more": {
|
||||
"_nested": "key"
|
||||
}
|
||||
});
|
||||
object.save().then( res => {
|
||||
ok(res);
|
||||
return res.fetch();
|
||||
}).then( res => {
|
||||
const foo = res.get("foo");
|
||||
expect(foo["_bar"]).toEqual("_");
|
||||
expect(foo["baz_bar"]).toEqual(1);
|
||||
expect(foo["__foo_bar"]).toBe(true);
|
||||
expect(foo["_0"]).toEqual("underscore_zero");
|
||||
expect(foo["_more"]["_nested"]).toEqual("key");
|
||||
done();
|
||||
}).fail( err => {
|
||||
console.error(err);
|
||||
fail("should not fail");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should have undefined includes when object is missing', (done) => {
|
||||
let obj1 = new Parse.Object("AnObject");
|
||||
let obj2 = new Parse.Object("AnObject");
|
||||
|
||||
Parse.Object.saveAll([obj1, obj2]).then(() => {
|
||||
obj1.set("obj", obj2);
|
||||
// Save the pointer, delete the pointee
|
||||
return obj1.save().then(() => { return obj2.destroy() });
|
||||
}).then(() => {
|
||||
let query = new Parse.Query("AnObject");
|
||||
query.include("obj");
|
||||
return query.find();
|
||||
}).then((res) => {
|
||||
expect(res.length).toBe(1);
|
||||
expect(res[0].get("obj")).toBe(undefined);
|
||||
let query = new Parse.Query("AnObject");
|
||||
return query.find();
|
||||
}).then((res) => {
|
||||
expect(res.length).toBe(1);
|
||||
expect(res[0].get("obj")).not.toBe(undefined);
|
||||
return res[0].get("obj").fetch();
|
||||
}).then(() => {
|
||||
fail("Should not fetch a deleted object");
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should have undefined includes when object is missing on deeper path', (done) => {
|
||||
let obj1 = new Parse.Object("AnObject");
|
||||
let obj2 = new Parse.Object("AnObject");
|
||||
let obj3 = new Parse.Object("AnObject");
|
||||
Parse.Object.saveAll([obj1, obj2, obj3]).then(() => {
|
||||
obj1.set("obj", obj2);
|
||||
obj2.set("obj", obj3);
|
||||
// Save the pointer, delete the pointee
|
||||
return Parse.Object.saveAll([obj1, obj2]).then(() => { return obj3.destroy() });
|
||||
}).then(() => {
|
||||
let query = new Parse.Query("AnObject");
|
||||
query.include("obj.obj");
|
||||
return query.get(obj1.id);
|
||||
}).then((res) => {
|
||||
expect(res.get("obj")).not.toBe(undefined);
|
||||
expect(res.get("obj").get("obj")).toBe(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
var ParsePubSub = require('../src/LiveQuery/ParsePubSub').ParsePubSub;
|
||||
|
||||
describe('ParsePubSub', function() {
|
||||
|
||||
beforeEach(function(done) {
|
||||
// Mock RedisPubSub
|
||||
var mockRedisPubSub = {
|
||||
createPublisher: jasmine.createSpy('createPublisherRedis'),
|
||||
createSubscriber: jasmine.createSpy('createSubscriberRedis')
|
||||
};
|
||||
jasmine.mockLibrary('../src/LiveQuery/RedisPubSub', 'RedisPubSub', mockRedisPubSub);
|
||||
// Mock EventEmitterPubSub
|
||||
var mockEventEmitterPubSub = {
|
||||
createPublisher: jasmine.createSpy('createPublisherEventEmitter'),
|
||||
createSubscriber: jasmine.createSpy('createSubscriberEventEmitter')
|
||||
};
|
||||
jasmine.mockLibrary('../src/LiveQuery/EventEmitterPubSub', 'EventEmitterPubSub', mockEventEmitterPubSub);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can create redis publisher', function() {
|
||||
var publisher = ParsePubSub.createPublisher({
|
||||
redisURL: 'redisURL'
|
||||
});
|
||||
|
||||
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
|
||||
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
|
||||
expect(RedisPubSub.createPublisher).toHaveBeenCalledWith('redisURL');
|
||||
expect(EventEmitterPubSub.createPublisher).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can create event emitter publisher', function() {
|
||||
var publisher = ParsePubSub.createPublisher({});
|
||||
|
||||
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
|
||||
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
|
||||
expect(RedisPubSub.createPublisher).not.toHaveBeenCalled();
|
||||
expect(EventEmitterPubSub.createPublisher).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can create redis subscriber', function() {
|
||||
var subscriber = ParsePubSub.createSubscriber({
|
||||
redisURL: 'redisURL'
|
||||
});
|
||||
|
||||
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
|
||||
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
|
||||
expect(RedisPubSub.createSubscriber).toHaveBeenCalledWith('redisURL');
|
||||
expect(EventEmitterPubSub.createSubscriber).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can create event emitter subscriber', function() {
|
||||
var subscriptionInfos = ParsePubSub.createSubscriber({});
|
||||
|
||||
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
|
||||
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
|
||||
expect(RedisPubSub.createSubscriber).not.toHaveBeenCalled();
|
||||
expect(EventEmitterPubSub.createSubscriber).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
afterEach(function(){
|
||||
jasmine.restoreLibrary('../src/LiveQuery/RedisPubSub', 'RedisPubSub');
|
||||
jasmine.restoreLibrary('../src/LiveQuery/EventEmitterPubSub', 'EventEmitterPubSub');
|
||||
});
|
||||
});
|
||||
@@ -1,150 +0,0 @@
|
||||
var ParsePushAdapter = require('../src/Adapters/Push/ParsePushAdapter');
|
||||
var APNS = require('../src/APNS');
|
||||
var GCM = require('../src/GCM');
|
||||
|
||||
describe('ParsePushAdapter', () => {
|
||||
it('can be initialized', (done) => {
|
||||
// Make mock config
|
||||
var pushConfig = {
|
||||
android: {
|
||||
senderId: 'senderId',
|
||||
apiKey: 'apiKey'
|
||||
},
|
||||
ios: [
|
||||
{
|
||||
cert: 'prodCert.pem',
|
||||
key: 'prodKey.pem',
|
||||
production: true,
|
||||
bundleId: 'bundleId'
|
||||
},
|
||||
{
|
||||
cert: 'devCert.pem',
|
||||
key: 'devKey.pem',
|
||||
production: false,
|
||||
bundleId: 'bundleIdAgain'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var parsePushAdapter = new ParsePushAdapter(pushConfig);
|
||||
// Check ios
|
||||
var iosSender = parsePushAdapter.senderMap['ios'];
|
||||
expect(iosSender instanceof APNS).toBe(true);
|
||||
// Check android
|
||||
var androidSender = parsePushAdapter.senderMap['android'];
|
||||
expect(androidSender instanceof GCM).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can throw on initializing with unsupported push type', (done) => {
|
||||
// Make mock config
|
||||
var pushConfig = {
|
||||
win: {
|
||||
senderId: 'senderId',
|
||||
apiKey: 'apiKey'
|
||||
}
|
||||
};
|
||||
|
||||
expect(function() {
|
||||
new ParsePushAdapter(pushConfig);
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can get valid push types', (done) => {
|
||||
var parsePushAdapter = new ParsePushAdapter();
|
||||
|
||||
expect(parsePushAdapter.getValidPushTypes()).toEqual(['ios', 'android']);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can classify installation', (done) => {
|
||||
// Mock installations
|
||||
var validPushTypes = ['ios', 'android'];
|
||||
var installations = [
|
||||
{
|
||||
deviceType: 'android',
|
||||
deviceToken: 'androidToken'
|
||||
},
|
||||
{
|
||||
deviceType: 'ios',
|
||||
deviceToken: 'iosToken'
|
||||
},
|
||||
{
|
||||
deviceType: 'win',
|
||||
deviceToken: 'winToken'
|
||||
},
|
||||
{
|
||||
deviceType: 'android',
|
||||
deviceToken: undefined
|
||||
}
|
||||
];
|
||||
|
||||
var deviceMap = ParsePushAdapter.classifyInstallation(installations, validPushTypes);
|
||||
expect(deviceMap['android']).toEqual([makeDevice('androidToken')]);
|
||||
expect(deviceMap['ios']).toEqual([makeDevice('iosToken')]);
|
||||
expect(deviceMap['win']).toBe(undefined);
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
it('can send push notifications', (done) => {
|
||||
var parsePushAdapter = new ParsePushAdapter();
|
||||
// Mock android ios senders
|
||||
var androidSender = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
var iosSender = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
var senderMap = {
|
||||
ios: iosSender,
|
||||
android: androidSender
|
||||
};
|
||||
parsePushAdapter.senderMap = senderMap;
|
||||
// Mock installations
|
||||
var installations = [
|
||||
{
|
||||
deviceType: 'android',
|
||||
deviceToken: 'androidToken'
|
||||
},
|
||||
{
|
||||
deviceType: 'ios',
|
||||
deviceToken: 'iosToken'
|
||||
},
|
||||
{
|
||||
deviceType: 'win',
|
||||
deviceToken: 'winToken'
|
||||
},
|
||||
{
|
||||
deviceType: 'android',
|
||||
deviceToken: undefined
|
||||
}
|
||||
];
|
||||
var data = {};
|
||||
|
||||
parsePushAdapter.send(data, installations);
|
||||
// Check android sender
|
||||
expect(androidSender.send).toHaveBeenCalled();
|
||||
var args = androidSender.send.calls.first().args;
|
||||
expect(args[0]).toEqual(data);
|
||||
expect(args[1]).toEqual([
|
||||
makeDevice('androidToken')
|
||||
]);
|
||||
// Check ios sender
|
||||
expect(iosSender.send).toHaveBeenCalled();
|
||||
args = iosSender.send.calls.first().args;
|
||||
expect(args[0]).toEqual(data);
|
||||
expect(args[1]).toEqual([
|
||||
makeDevice('iosToken')
|
||||
]);
|
||||
done();
|
||||
});
|
||||
|
||||
function makeDevice(deviceToken, appIdentifier) {
|
||||
return {
|
||||
deviceToken: deviceToken,
|
||||
appIdentifier: appIdentifier
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -2,6 +2,9 @@
|
||||
// hungry/js/test/parse_query_test.js
|
||||
//
|
||||
// Some new tests are added.
|
||||
'use strict';
|
||||
|
||||
const Parse = require('parse/node');
|
||||
|
||||
describe('Parse.Query testing', () => {
|
||||
it("basic query", function(done) {
|
||||
@@ -20,6 +23,105 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("notEqualTo with Relation is working", function(done) {
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
|
||||
var user1 = new Parse.User();
|
||||
user1.setPassword("asdf");
|
||||
user1.setUsername("qwerty");
|
||||
|
||||
var user2 = new Parse.User();
|
||||
user2.setPassword("asdf");
|
||||
user2.setUsername("asdf");
|
||||
|
||||
var Cake = Parse.Object.extend("Cake");
|
||||
var cake1 = new Cake();
|
||||
var cake2 = new Cake();
|
||||
var cake3 = new Cake();
|
||||
|
||||
|
||||
user.signUp().then(function(){
|
||||
return user1.signUp();
|
||||
}).then(function(){
|
||||
return user2.signUp();
|
||||
}).then(function(){
|
||||
var relLike1 = cake1.relation("liker");
|
||||
relLike1.add([user, user1]);
|
||||
|
||||
var relDislike1 = cake1.relation("hater");
|
||||
relDislike1.add(user2);
|
||||
return cake1.save();
|
||||
}).then(function(){
|
||||
var rellike2 = cake2.relation("liker");
|
||||
rellike2.add([user, user1]);
|
||||
|
||||
var relDislike2 = cake2.relation("hater");
|
||||
relDislike2.add(user2);
|
||||
|
||||
return cake2.save();
|
||||
}).then(function(){
|
||||
var rellike3 = cake3.relation("liker");
|
||||
rellike3.add(user);
|
||||
|
||||
var relDislike3 = cake3.relation("hater");
|
||||
relDislike3.add([user1, user2]);
|
||||
return cake3.save();
|
||||
}).then(function(){
|
||||
var query = new Parse.Query(Cake);
|
||||
// User2 likes nothing so we should receive 0
|
||||
query.equalTo("liker", user2);
|
||||
return query.find().then(function(results){
|
||||
equal(results.length, 0);
|
||||
});
|
||||
}).then(function(){
|
||||
var query = new Parse.Query(Cake);
|
||||
// User1 likes two of three cakes
|
||||
query.equalTo("liker", user1);
|
||||
return query.find().then(function(results){
|
||||
// It should return 2 -> cake 1 and cake 2
|
||||
equal(results.length, 2);
|
||||
});
|
||||
}).then(function(){
|
||||
var query = new Parse.Query(Cake);
|
||||
// We want to know which cake the user1 is not appreciating -> cake3
|
||||
query.notEqualTo("liker", user1);
|
||||
return query.find().then(function(results){
|
||||
// Should return 1 -> the cake 3
|
||||
equal(results.length, 1);
|
||||
});
|
||||
}).then(function(){
|
||||
var query = new Parse.Query(Cake);
|
||||
// User2 is a hater of everything so we should receive 0
|
||||
query.notEqualTo("hater", user2);
|
||||
return query.find().then(function(results){
|
||||
equal(results.length, 0);
|
||||
});
|
||||
}).then(function(){
|
||||
var query = new Parse.Query(Cake);
|
||||
// Only cake3 is liked by user
|
||||
query.notContainedIn("liker", [user1]);
|
||||
return query.find().then(function(results){
|
||||
equal(results.length, 1);
|
||||
});
|
||||
}).then(function(){
|
||||
var query = new Parse.Query(Cake);
|
||||
// All the users
|
||||
query.containedIn("liker", [user, user1, user2]);
|
||||
// Exclude user 1
|
||||
query.notEqualTo("liker", user1);
|
||||
// Only cake3 is liked only by user1
|
||||
return query.find().then(function(results){
|
||||
equal(results.length, 1);
|
||||
let cake = results[0];
|
||||
expect(cake.id).toBe(cake3.id);
|
||||
});
|
||||
}).then(function(){
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("query with limit", function(done) {
|
||||
var baz = new TestObject({ foo: 'baz' });
|
||||
var qux = new TestObject({ foo: 'qux' });
|
||||
@@ -1419,6 +1521,153 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('properly includes array', (done) => {
|
||||
let objects = [];
|
||||
let total = 0;
|
||||
while(objects.length != 5) {
|
||||
let object = new Parse.Object('AnObject');
|
||||
object.set('key', objects.length);
|
||||
total += objects.length;
|
||||
objects.push(object);
|
||||
}
|
||||
Parse.Object.saveAll(objects).then(() => {
|
||||
let object = new Parse.Object("AContainer");
|
||||
object.set('objects', objects);
|
||||
return object.save();
|
||||
}).then(() => {
|
||||
let query = new Parse.Query('AContainer');
|
||||
query.include('objects');
|
||||
return query.find()
|
||||
}).then((results) => {
|
||||
expect(results.length).toBe(1);
|
||||
let res = results[0];
|
||||
let objects = res.get('objects');
|
||||
expect(objects.length).toBe(5);
|
||||
objects.forEach((object) => {
|
||||
total -= object.get('key');
|
||||
});
|
||||
expect(total).toBe(0);
|
||||
done()
|
||||
}, () => {
|
||||
fail('should not fail');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('properly includes array of mixed objects', (done) => {
|
||||
let objects = [];
|
||||
let total = 0;
|
||||
while(objects.length != 5) {
|
||||
let object = new Parse.Object('AnObject');
|
||||
object.set('key', objects.length);
|
||||
total += objects.length;
|
||||
objects.push(object);
|
||||
}
|
||||
while(objects.length != 10) {
|
||||
let object = new Parse.Object('AnotherObject');
|
||||
object.set('key', objects.length);
|
||||
total += objects.length;
|
||||
objects.push(object);
|
||||
}
|
||||
Parse.Object.saveAll(objects).then(() => {
|
||||
let object = new Parse.Object("AContainer");
|
||||
object.set('objects', objects);
|
||||
return object.save();
|
||||
}).then(() => {
|
||||
let query = new Parse.Query('AContainer');
|
||||
query.include('objects');
|
||||
return query.find()
|
||||
}).then((results) => {
|
||||
expect(results.length).toBe(1);
|
||||
let res = results[0];
|
||||
let objects = res.get('objects');
|
||||
expect(objects.length).toBe(10);
|
||||
objects.forEach((object) => {
|
||||
total -= object.get('key');
|
||||
});
|
||||
expect(total).toBe(0);
|
||||
done()
|
||||
}, (err) => {
|
||||
fail('should not fail');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('properly nested array of mixed objects with bad ids', (done) => {
|
||||
let objects = [];
|
||||
let total = 0;
|
||||
while(objects.length != 5) {
|
||||
let object = new Parse.Object('AnObject');
|
||||
object.set('key', objects.length);
|
||||
objects.push(object);
|
||||
}
|
||||
while(objects.length != 10) {
|
||||
let object = new Parse.Object('AnotherObject');
|
||||
object.set('key', objects.length);
|
||||
objects.push(object);
|
||||
}
|
||||
Parse.Object.saveAll(objects).then(() => {
|
||||
let object = new Parse.Object("AContainer");
|
||||
for (var i=0; i<objects.length; i++) {
|
||||
if (i%2 == 0) {
|
||||
objects[i].id = 'randomThing'
|
||||
} else {
|
||||
total += objects[i].get('key');
|
||||
}
|
||||
}
|
||||
object.set('objects', objects);
|
||||
return object.save();
|
||||
}).then(() => {
|
||||
let query = new Parse.Query('AContainer');
|
||||
query.include('objects');
|
||||
return query.find()
|
||||
}).then((results) => {
|
||||
expect(results.length).toBe(1);
|
||||
let res = results[0];
|
||||
let objects = res.get('objects');
|
||||
expect(objects.length).toBe(5);
|
||||
objects.forEach((object) => {
|
||||
total -= object.get('key');
|
||||
});
|
||||
expect(total).toBe(0);
|
||||
done()
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail('should not fail');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('properly fetches nested pointers', (done) => {
|
||||
let color = new Parse.Object('Color');
|
||||
color.set('hex','#133733');
|
||||
let circle = new Parse.Object('Circle');
|
||||
circle.set('radius', 1337);
|
||||
|
||||
Parse.Object.saveAll([color, circle]).then(() => {
|
||||
circle.set('color', color);
|
||||
let badCircle = new Parse.Object('Circle');
|
||||
badCircle.id = 'badId';
|
||||
let complexFigure = new Parse.Object('ComplexFigure');
|
||||
complexFigure.set('consistsOf', [circle, badCircle]);
|
||||
return complexFigure.save();
|
||||
}).then(() => {
|
||||
let q = new Parse.Query('ComplexFigure');
|
||||
q.include('consistsOf.color');
|
||||
return q.find()
|
||||
}).then((results) => {
|
||||
expect(results.length).toBe(1);
|
||||
let figure = results[0];
|
||||
expect(figure.get('consistsOf').length).toBe(1);
|
||||
expect(figure.get('consistsOf')[0].get('color').get('hex')).toBe('#133733');
|
||||
done();
|
||||
}, (err) => {
|
||||
fail('should not fail');
|
||||
done();
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
it("result object creation uses current extension", function(done) {
|
||||
var ParentObject = Parse.Object.extend({ className: "ParentObject" });
|
||||
// Add a foo() method to ChildObject.
|
||||
@@ -1574,6 +1823,29 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("dontSelect query without conditions", function(done) {
|
||||
const RestaurantObject = Parse.Object.extend("Restaurant");
|
||||
const PersonObject = Parse.Object.extend("Person");
|
||||
const objects = [
|
||||
new RestaurantObject({ location: "Djibouti" }),
|
||||
new RestaurantObject({ location: "Ouagadougou" }),
|
||||
new PersonObject({ name: "Bob", hometown: "Djibouti" }),
|
||||
new PersonObject({ name: "Tom", hometown: "Yoloblahblahblah" }),
|
||||
new PersonObject({ name: "Billy", hometown: "Ouagadougou" })
|
||||
];
|
||||
|
||||
Parse.Object.saveAll(objects, function() {
|
||||
const query = new Parse.Query(RestaurantObject);
|
||||
const mainQuery = new Parse.Query(PersonObject);
|
||||
mainQuery.doesNotMatchKeyInQuery("hometown", "location", query);
|
||||
mainQuery.find().then(results => {
|
||||
equal(results.length, 1);
|
||||
equal(results[0].get('name'), 'Tom');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("object with length", function(done) {
|
||||
var TestObject = Parse.Object.extend("TestObject");
|
||||
var obj = new TestObject();
|
||||
@@ -2088,4 +2360,176 @@ describe('Parse.Query testing', () => {
|
||||
console.log(error);
|
||||
});
|
||||
});
|
||||
|
||||
// #371
|
||||
it('should properly interpret a query', (done) => {
|
||||
var query = new Parse.Query("C1");
|
||||
var auxQuery = new Parse.Query("C1");
|
||||
query.matchesKeyInQuery("A1", "A2", auxQuery);
|
||||
query.include("A3");
|
||||
query.include("A2");
|
||||
query.find().then((result) => {
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("should not failt");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should properly interpret a query', (done) => {
|
||||
var user = new Parse.User();
|
||||
user.set("username", "foo");
|
||||
user.set("password", "bar");
|
||||
return user.save().then( (user) => {
|
||||
var objIdQuery = new Parse.Query("_User").equalTo("objectId", user.id);
|
||||
var blockedUserQuery = user.relation("blockedUsers").query();
|
||||
|
||||
var aResponseQuery = new Parse.Query("MatchRelationshipActivityResponse");
|
||||
aResponseQuery.equalTo("userA", user);
|
||||
aResponseQuery.equalTo("userAResponse", 1);
|
||||
|
||||
var bResponseQuery = new Parse.Query("MatchRelationshipActivityResponse");
|
||||
bResponseQuery.equalTo("userB", user);
|
||||
bResponseQuery.equalTo("userBResponse", 1);
|
||||
|
||||
var matchOr = Parse.Query.or(aResponseQuery, bResponseQuery);
|
||||
var matchRelationshipA = new Parse.Query("_User");
|
||||
matchRelationshipA.matchesKeyInQuery("objectId", "userAObjectId", matchOr);
|
||||
var matchRelationshipB = new Parse.Query("_User");
|
||||
matchRelationshipB.matchesKeyInQuery("objectId", "userBObjectId", matchOr);
|
||||
|
||||
|
||||
var orQuery = Parse.Query.or(objIdQuery, blockedUserQuery, matchRelationshipA, matchRelationshipB);
|
||||
var query = new Parse.Query("_User");
|
||||
query.doesNotMatchQuery("objectId", orQuery);
|
||||
return query.find();
|
||||
}).then((res) => {
|
||||
done();
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("should not fail");
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
it('should find objects with array of pointers', (done) => {
|
||||
var objects = [];
|
||||
while(objects.length != 5) {
|
||||
var object = new Parse.Object('ContainedObject');
|
||||
object.set('index', objects.length);
|
||||
objects.push(object);
|
||||
}
|
||||
|
||||
Parse.Object.saveAll(objects).then((objects) => {
|
||||
var container = new Parse.Object('Container');
|
||||
var pointers = objects.map((obj) => {
|
||||
return {
|
||||
__type: 'Pointer',
|
||||
className: 'ContainedObject',
|
||||
objectId: obj.id
|
||||
}
|
||||
})
|
||||
container.set('objects', pointers);
|
||||
let container2 = new Parse.Object('Container');
|
||||
container2.set('objects', pointers.slice(2, 3));
|
||||
return Parse.Object.saveAll([container, container2]);
|
||||
}).then(() => {
|
||||
let inQuery = new Parse.Query('ContainedObject');
|
||||
inQuery.greaterThanOrEqualTo('index', 1);
|
||||
let query = new Parse.Query('Container');
|
||||
query.matchesQuery('objects', inQuery);
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
if (results) {
|
||||
expect(results.length).toBe(2);
|
||||
}
|
||||
done();
|
||||
}).fail((err) => {
|
||||
console.error(err);
|
||||
fail('should not fail');
|
||||
done();
|
||||
})
|
||||
})
|
||||
|
||||
it('query with two OR subqueries (regression test #1259)', done => {
|
||||
let relatedObject = new Parse.Object('Class2');
|
||||
relatedObject.save().then(relatedObject => {
|
||||
let anObject = new Parse.Object('Class1');
|
||||
let relation = anObject.relation('relation');
|
||||
relation.add(relatedObject);
|
||||
return anObject.save();
|
||||
}).then(anObject => {
|
||||
let q1 = anObject.relation('relation').query();
|
||||
q1.doesNotExist('nonExistantKey1');
|
||||
let q2 = anObject.relation('relation').query();
|
||||
q2.doesNotExist('nonExistantKey2');
|
||||
let orQuery = Parse.Query.or(q1, q2).find().then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0].objectId).toEqual(q1.objectId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('objectId containedIn with multiple large array', done => {
|
||||
let obj = new Parse.Object('MyClass');
|
||||
obj.save().then(obj => {
|
||||
let longListOfStrings = [];
|
||||
for (let i = 0; i < 130; i++) {
|
||||
longListOfStrings.push(i.toString());
|
||||
}
|
||||
longListOfStrings.push(obj.id);
|
||||
let q = new Parse.Query('MyClass');
|
||||
q.containedIn('objectId', longListOfStrings);
|
||||
q.containedIn('objectId', longListOfStrings);
|
||||
return q.find();
|
||||
}).then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('include for specific object', function(done){
|
||||
var child = new Parse.Object('Child');
|
||||
var parent = new Parse.Object('Parent');
|
||||
child.set('foo', 'bar');
|
||||
parent.set('child', child);
|
||||
Parse.Object.saveAll([child, parent], function(response){
|
||||
var savedParent = response[1];
|
||||
var parentQuery = new Parse.Query('Parent');
|
||||
parentQuery.include('child');
|
||||
parentQuery.get(savedParent.id, {
|
||||
success: function(parentObj) {
|
||||
var childPointer = parentObj.get('child');
|
||||
ok(childPointer);
|
||||
equal(childPointer.get('foo'), 'bar');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('select keys for specific object', function(done){
|
||||
var Foobar = new Parse.Object('Foobar');
|
||||
Foobar.set('foo', 'bar');
|
||||
Foobar.set('fizz', 'buzz');
|
||||
Foobar.save({
|
||||
success: function(savedFoobar){
|
||||
var foobarQuery = new Parse.Query('Foobar');
|
||||
foobarQuery.select('fizz');
|
||||
foobarQuery.get(savedFoobar.id,{
|
||||
success: function(foobarObj){
|
||||
equal(foobarObj.get('fizz'), 'buzz');
|
||||
equal(foobarObj.get('foo'), undefined);
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
+349
-1
@@ -1,3 +1,4 @@
|
||||
'use strict';
|
||||
// This is a port of the test suite:
|
||||
// hungry/js/test/parse_relation_test.js
|
||||
|
||||
@@ -247,6 +248,177 @@ describe('Parse.Relation testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("queries on relation fields with multiple containedIn (regression test for #1271)", (done) => {
|
||||
let ChildObject = Parse.Object.extend("ChildObject");
|
||||
let childObjects = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
childObjects.push(new ChildObject({x: i}));
|
||||
}
|
||||
|
||||
Parse.Object.saveAll(childObjects).then(() => {
|
||||
let ParentObject = Parse.Object.extend("ParentObject");
|
||||
let parent = new ParentObject();
|
||||
parent.set("x", 4);
|
||||
let parent1Children = parent.relation("child");
|
||||
parent1Children.add(childObjects[0]);
|
||||
parent1Children.add(childObjects[1]);
|
||||
parent1Children.add(childObjects[2]);
|
||||
let parent2 = new ParentObject();
|
||||
parent2.set("x", 3);
|
||||
let parent2Children = parent2.relation("child");
|
||||
parent2Children.add(childObjects[4]);
|
||||
parent2Children.add(childObjects[5]);
|
||||
parent2Children.add(childObjects[6]);
|
||||
|
||||
let parent2OtherChildren = parent2.relation("otherChild");
|
||||
parent2OtherChildren.add(childObjects[0]);
|
||||
parent2OtherChildren.add(childObjects[1]);
|
||||
parent2OtherChildren.add(childObjects[2]);
|
||||
|
||||
return Parse.Object.saveAll([parent, parent2]);
|
||||
}).then(() => {
|
||||
let objectsWithChild0InBothChildren = new Parse.Query(ParentObject);
|
||||
objectsWithChild0InBothChildren.containedIn("child", [childObjects[0]]);
|
||||
objectsWithChild0InBothChildren.containedIn("otherChild", [childObjects[0]]);
|
||||
return objectsWithChild0InBothChildren.find();
|
||||
}).then(objectsWithChild0InBothChildren => {
|
||||
//No parent has child 0 in both it's "child" and "otherChild" field;
|
||||
expect(objectsWithChild0InBothChildren.length).toEqual(0);
|
||||
}).then(() => {
|
||||
let objectsWithChild4andOtherChild1 = new Parse.Query(ParentObject);
|
||||
objectsWithChild4andOtherChild1.containedIn("child", [childObjects[4]]);
|
||||
objectsWithChild4andOtherChild1.containedIn("otherChild", [childObjects[1]]);
|
||||
return objectsWithChild4andOtherChild1.find();
|
||||
}).then(objects => {
|
||||
// parent2 has child 4 and otherChild 1
|
||||
expect(objects.length).toEqual(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("query on pointer and relation fields with equal", (done) => {
|
||||
var ChildObject = Parse.Object.extend("ChildObject");
|
||||
var childObjects = [];
|
||||
for (var i = 0; i < 10; i++) {
|
||||
childObjects.push(new ChildObject({x: i}));
|
||||
}
|
||||
|
||||
Parse.Object.saveAll(childObjects).then(() => {
|
||||
var ParentObject = Parse.Object.extend("ParentObject");
|
||||
var parent = new ParentObject();
|
||||
parent.set("x", 4);
|
||||
var relation = parent.relation("toChilds");
|
||||
relation.add(childObjects[0]);
|
||||
relation.add(childObjects[1]);
|
||||
relation.add(childObjects[2]);
|
||||
|
||||
var parent2 = new ParentObject();
|
||||
parent2.set("x", 3);
|
||||
parent2.set("toChild", childObjects[2]);
|
||||
|
||||
var parents = [];
|
||||
parents.push(parent);
|
||||
parents.push(parent2);
|
||||
parents.push(new ParentObject());
|
||||
|
||||
return Parse.Object.saveAll(parents).then(() => {
|
||||
var query = new Parse.Query(ParentObject);
|
||||
query.equalTo("objectId", parent.id);
|
||||
query.equalTo("toChilds", childObjects[2]);
|
||||
|
||||
return query.find().then((list) => {
|
||||
equal(list.length, 1, "There should be 1 result");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("query on pointer and relation fields with equal bis", (done) => {
|
||||
var ChildObject = Parse.Object.extend("ChildObject");
|
||||
var childObjects = [];
|
||||
for (var i = 0; i < 10; i++) {
|
||||
childObjects.push(new ChildObject({x: i}));
|
||||
}
|
||||
|
||||
Parse.Object.saveAll(childObjects).then(() => {
|
||||
var ParentObject = Parse.Object.extend("ParentObject");
|
||||
var parent = new ParentObject();
|
||||
parent.set("x", 4);
|
||||
var relation = parent.relation("toChilds");
|
||||
relation.add(childObjects[0]);
|
||||
relation.add(childObjects[1]);
|
||||
relation.add(childObjects[2]);
|
||||
|
||||
var parent2 = new ParentObject();
|
||||
parent2.set("x", 3);
|
||||
parent2.relation("toChilds").add(childObjects[2]);
|
||||
|
||||
var parents = [];
|
||||
parents.push(parent);
|
||||
parents.push(parent2);
|
||||
parents.push(new ParentObject());
|
||||
|
||||
return Parse.Object.saveAll(parents).then(() => {
|
||||
var query = new Parse.Query(ParentObject);
|
||||
query.equalTo("objectId", parent2.id);
|
||||
// childObjects[2] is in 2 relations
|
||||
// before the fix, that woul yield 2 results
|
||||
query.equalTo("toChilds", childObjects[2]);
|
||||
|
||||
return query.find().then((list) => {
|
||||
equal(list.length, 1, "There should be 1 result");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("or queries on pointer and relation fields", (done) => {
|
||||
var ChildObject = Parse.Object.extend("ChildObject");
|
||||
var childObjects = [];
|
||||
for (var i = 0; i < 10; i++) {
|
||||
childObjects.push(new ChildObject({x: i}));
|
||||
}
|
||||
|
||||
Parse.Object.saveAll(childObjects).then(() => {
|
||||
var ParentObject = Parse.Object.extend("ParentObject");
|
||||
var parent = new ParentObject();
|
||||
parent.set("x", 4);
|
||||
var relation = parent.relation("toChilds");
|
||||
relation.add(childObjects[0]);
|
||||
relation.add(childObjects[1]);
|
||||
relation.add(childObjects[2]);
|
||||
|
||||
var parent2 = new ParentObject();
|
||||
parent2.set("x", 3);
|
||||
parent2.set("toChild", childObjects[2]);
|
||||
|
||||
var parents = [];
|
||||
parents.push(parent);
|
||||
parents.push(parent2);
|
||||
parents.push(new ParentObject());
|
||||
|
||||
return Parse.Object.saveAll(parents).then(() => {
|
||||
var query1 = new Parse.Query(ParentObject);
|
||||
query1.containedIn("toChilds", [childObjects[2]]);
|
||||
var query2 = new Parse.Query(ParentObject);
|
||||
query2.equalTo("toChild", childObjects[2]);
|
||||
var query = Parse.Query.or(query1, query2);
|
||||
return query.find().then((list) => {
|
||||
var objectIds = list.map(function(item){
|
||||
return item.id;
|
||||
});
|
||||
expect(objectIds.indexOf(parent.id)).not.toBe(-1);
|
||||
expect(objectIds.indexOf(parent2.id)).not.toBe(-1);
|
||||
equal(list.length, 2, "There should be 2 results");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("Get query on relation using un-fetched parent object", (done) => {
|
||||
// Setup data model
|
||||
var Wheel = Parse.Object.extend('Wheel');
|
||||
@@ -334,5 +506,181 @@ describe('Parse.Relation testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
it('should properly get related objects with unfetched queries', (done) => {
|
||||
let objects = [];
|
||||
let owners = [];
|
||||
let allObjects = [];
|
||||
// Build 10 Objects and 10 owners
|
||||
while (objects.length != 10) {
|
||||
let object = new Parse.Object('AnObject');
|
||||
object.set({
|
||||
index: objects.length,
|
||||
even: objects.length % 2 == 0
|
||||
});
|
||||
objects.push(object);
|
||||
let owner = new Parse.Object('AnOwner');
|
||||
owners.push(owner);
|
||||
allObjects.push(object);
|
||||
allObjects.push(owner);
|
||||
}
|
||||
|
||||
let anotherOwner = new Parse.Object('AnotherOwner');
|
||||
|
||||
return Parse.Object.saveAll(allObjects.concat([anotherOwner])).then(() => {
|
||||
// put all the AnObject into the anotherOwner relationKey
|
||||
anotherOwner.relation('relationKey').add(objects);
|
||||
// Set each object[i] into owner[i];
|
||||
owners.forEach((owner,i) => {
|
||||
owner.set('key', objects[i]);
|
||||
});
|
||||
return Parse.Object.saveAll(owners.concat([anotherOwner]));
|
||||
}).then(() => {
|
||||
// Query on the relation of another owner
|
||||
let object = new Parse.Object('AnotherOwner');
|
||||
object.id = anotherOwner.id;
|
||||
let relationQuery = object.relation('relationKey').query();
|
||||
// Just get the even ones
|
||||
relationQuery.equalTo('even', true);
|
||||
// Make the query on anOwner
|
||||
let query = new Parse.Query('AnOwner');
|
||||
// where key match the relation query.
|
||||
query.matchesQuery('key', relationQuery);
|
||||
query.include('key');
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
expect(results.length).toBe(5);
|
||||
results.forEach((result) => {
|
||||
expect(result.get('key').get('even')).toBe(true);
|
||||
});
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
// Query on the relation of another owner
|
||||
let object = new Parse.Object('AnotherOwner');
|
||||
object.id = anotherOwner.id;
|
||||
let relationQuery = object.relation('relationKey').query();
|
||||
// Just get the even ones
|
||||
relationQuery.equalTo('even', true);
|
||||
// Make the query on anOwner
|
||||
let query = new Parse.Query('AnOwner');
|
||||
// where key match the relation query.
|
||||
query.doesNotMatchQuery('key', relationQuery);
|
||||
query.include('key');
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
expect(results.length).toBe(5);
|
||||
results.forEach((result) => {
|
||||
expect(result.get('key').get('even')).toBe(false);
|
||||
});
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("select query", function(done) {
|
||||
var RestaurantObject = Parse.Object.extend("Restaurant");
|
||||
var PersonObject = Parse.Object.extend("Person");
|
||||
var OwnerObject = Parse.Object.extend('Owner');
|
||||
var restaurants = [
|
||||
new RestaurantObject({ ratings: 5, location: "Djibouti" }),
|
||||
new RestaurantObject({ ratings: 3, location: "Ouagadougou" }),
|
||||
];
|
||||
let persons = [
|
||||
new PersonObject({ name: "Bob", hometown: "Djibouti" }),
|
||||
new PersonObject({ name: "Tom", hometown: "Ouagadougou" }),
|
||||
new PersonObject({ name: "Billy", hometown: "Detroit" }),
|
||||
];
|
||||
let owner = new OwnerObject({name: 'Joe'});
|
||||
let ownerId;
|
||||
let allObjects = [owner].concat(restaurants).concat(persons);
|
||||
expect(allObjects.length).toEqual(6);
|
||||
Parse.Object.saveAll([owner].concat(restaurants).concat(persons)).then(function() {
|
||||
ownerId = owner.id;
|
||||
owner.relation('restaurants').add(restaurants);
|
||||
return owner.save()
|
||||
}).then(() => {
|
||||
let unfetchedOwner = new OwnerObject();
|
||||
unfetchedOwner.id = owner.id;
|
||||
var query = unfetchedOwner.relation('restaurants').query();
|
||||
query.greaterThan("ratings", 4);
|
||||
var mainQuery = new Parse.Query(PersonObject);
|
||||
mainQuery.matchesKeyInQuery("hometown", "location", query);
|
||||
mainQuery.find(expectSuccess({
|
||||
success: function(results) {
|
||||
equal(results.length, 1);
|
||||
if (results.length > 0) {
|
||||
equal(results[0].get('name'), 'Bob');
|
||||
}
|
||||
done();
|
||||
}
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
it("dontSelect query", function(done) {
|
||||
var RestaurantObject = Parse.Object.extend("Restaurant");
|
||||
var PersonObject = Parse.Object.extend("Person");
|
||||
var OwnerObject = Parse.Object.extend('Owner');
|
||||
var restaurants = [
|
||||
new RestaurantObject({ ratings: 5, location: "Djibouti" }),
|
||||
new RestaurantObject({ ratings: 3, location: "Ouagadougou" }),
|
||||
];
|
||||
let persons = [
|
||||
new PersonObject({ name: "Bob", hometown: "Djibouti" }),
|
||||
new PersonObject({ name: "Tom", hometown: "Ouagadougou" }),
|
||||
new PersonObject({ name: "Billy", hometown: "Detroit" }),
|
||||
];
|
||||
let owner = new OwnerObject({name: 'Joe'});
|
||||
let ownerId;
|
||||
let allObjects = [owner].concat(restaurants).concat(persons);
|
||||
expect(allObjects.length).toEqual(6);
|
||||
Parse.Object.saveAll([owner].concat(restaurants).concat(persons)).then(function() {
|
||||
ownerId = owner.id;
|
||||
owner.relation('restaurants').add(restaurants);
|
||||
return owner.save()
|
||||
}).then(() => {
|
||||
let unfetchedOwner = new OwnerObject();
|
||||
unfetchedOwner.id = owner.id;
|
||||
var query = unfetchedOwner.relation('restaurants').query();
|
||||
query.greaterThan("ratings", 4);
|
||||
var mainQuery = new Parse.Query(PersonObject);
|
||||
mainQuery.doesNotMatchKeyInQuery("hometown", "location", query);
|
||||
mainQuery.ascending('name');
|
||||
mainQuery.find(expectSuccess({
|
||||
success: function(results) {
|
||||
equal(results.length, 2);
|
||||
if (results.length > 0) {
|
||||
equal(results[0].get('name'), 'Billy');
|
||||
equal(results[1].get('name'), 'Tom');
|
||||
}
|
||||
done();
|
||||
}
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
it('relations are not bidirectional (regression test for #871)', done => {
|
||||
let PersonObject = Parse.Object.extend("Person");
|
||||
let p1 = new PersonObject();
|
||||
let p2 = new PersonObject();
|
||||
Parse.Object.saveAll([p1, p2]).then(results => {
|
||||
let p1 = results[0];
|
||||
let p2 = results[1];
|
||||
let relation = p1.relation('relation');
|
||||
relation.add(p2);
|
||||
p1.save().then(() => {
|
||||
let query = new Parse.Query(PersonObject);
|
||||
query.equalTo('relation', p1);
|
||||
query.find().then(results => {
|
||||
expect(results.length).toEqual(0);
|
||||
|
||||
let query = new Parse.Query(PersonObject);
|
||||
query.equalTo('relation', p2);
|
||||
query.find().then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0].objectId).toEqual(p1.objectId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+287
-2
@@ -1,7 +1,10 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
// Roles are not accessible without the master key, so they are not intended
|
||||
// for use by clients. We can manually test them using the master key.
|
||||
var RestQuery = require("../src/RestQuery");
|
||||
var Auth = require("../src/Auth").Auth;
|
||||
var Config = require("../src/Config");
|
||||
|
||||
describe('Parse Role testing', () => {
|
||||
|
||||
@@ -49,7 +52,7 @@ describe('Parse Role testing', () => {
|
||||
}).then((x) => {
|
||||
x.set('foo', 'baz');
|
||||
// This should fail:
|
||||
return x.save();
|
||||
return x.save({},{sessionToken: ""});
|
||||
}).then((x) => {
|
||||
fail('Should not have been able to save.');
|
||||
}, (e) => {
|
||||
@@ -58,5 +61,287 @@ describe('Parse Role testing', () => {
|
||||
|
||||
});
|
||||
|
||||
var createRole = function(name, sibling, user) {
|
||||
var role = new Parse.Role(name, new Parse.ACL());
|
||||
if (user) {
|
||||
var users = role.relation('users');
|
||||
users.add(user);
|
||||
}
|
||||
if (sibling) {
|
||||
role.relation('roles').add(sibling);
|
||||
}
|
||||
return role.save({}, { useMasterKey: true });
|
||||
};
|
||||
|
||||
it("should not recursively load the same role multiple times", (done) => {
|
||||
var rootRole = "RootRole";
|
||||
var roleNames = ["FooRole", "BarRole", "BazRole"];
|
||||
var allRoles = [rootRole].concat(roleNames);
|
||||
|
||||
var roleObjs = {};
|
||||
var createAllRoles = function(user) {
|
||||
var promises = allRoles.map(function(roleName) {
|
||||
return createRole(roleName, null, user)
|
||||
.then(function(roleObj) {
|
||||
roleObjs[roleName] = roleObj;
|
||||
return roleObj;
|
||||
});
|
||||
});
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
var restExecute = spyOn(RestQuery.prototype, "execute").and.callThrough();
|
||||
|
||||
var user,
|
||||
auth,
|
||||
getAllRolesSpy;
|
||||
createTestUser().then( (newUser) => {
|
||||
user = newUser;
|
||||
return createAllRoles(user);
|
||||
}).then ( (roles) => {
|
||||
var rootRoleObj = roleObjs[rootRole];
|
||||
roles.forEach(function(role, i) {
|
||||
// Add all roles to the RootRole
|
||||
if (role.id !== rootRoleObj.id) {
|
||||
role.relation("roles").add(rootRoleObj);
|
||||
}
|
||||
// Add all "roleNames" roles to the previous role
|
||||
if (i > 0) {
|
||||
role.relation("roles").add(roles[i - 1]);
|
||||
}
|
||||
});
|
||||
|
||||
return Parse.Object.saveAll(roles, { useMasterKey: true });
|
||||
}).then( () => {
|
||||
auth = new Auth({config: new Config("test"), isMaster: true, user: user});
|
||||
getAllRolesSpy = spyOn(auth, "_getAllRoleNamesForId").and.callThrough();
|
||||
|
||||
return auth._loadRoles();
|
||||
}).then ( (roles) => {
|
||||
expect(roles.length).toEqual(4);
|
||||
|
||||
allRoles.forEach(function(name) {
|
||||
expect(roles.indexOf("role:"+name)).not.toBe(-1);
|
||||
});
|
||||
|
||||
// 1 Query for the initial setup
|
||||
// 4 Queries for all the specific roles
|
||||
// 1 Query for the final $in
|
||||
expect(restExecute.calls.count()).toEqual(6);
|
||||
|
||||
// 4 One query for each of the roles
|
||||
// 3 Queries for the "RootRole"
|
||||
expect(getAllRolesSpy.calls.count()).toEqual(7);
|
||||
done()
|
||||
}).catch( () => {
|
||||
fail("should succeed");
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it("should recursively load roles", (done) => {
|
||||
var rolesNames = ["FooRole", "BarRole", "BazRole"];
|
||||
var roleIds = {};
|
||||
createTestUser().then( (user) => {
|
||||
// Put the user on the 1st role
|
||||
return createRole(rolesNames[0], null, user).then( (aRole) => {
|
||||
roleIds[aRole.get("name")] = aRole.id;
|
||||
// set the 1st role as a sibling of the second
|
||||
// user will should have 2 role now
|
||||
return createRole(rolesNames[1], aRole, null);
|
||||
}).then( (anotherRole) => {
|
||||
roleIds[anotherRole.get("name")] = anotherRole.id;
|
||||
// set this role as a sibling of the last
|
||||
// the user should now have 3 roles
|
||||
return createRole(rolesNames[2], anotherRole, null);
|
||||
}).then( (lastRole) => {
|
||||
roleIds[lastRole.get("name")] = lastRole.id;
|
||||
var auth = new Auth({ config: new Config("test"), isMaster: true, user: user });
|
||||
return auth._loadRoles();
|
||||
})
|
||||
}).then( (roles) => {
|
||||
expect(roles.length).toEqual(3);
|
||||
rolesNames.forEach( (name) => {
|
||||
expect(roles.indexOf('role:'+name)).not.toBe(-1);
|
||||
});
|
||||
done();
|
||||
}, function(err){
|
||||
fail("should succeed")
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("_Role object should not save without name.", (done) => {
|
||||
var role = new Parse.Role();
|
||||
role.save(null,{useMasterKey:true})
|
||||
.then((r) => {
|
||||
fail("_Role object should not save without name.");
|
||||
}, (error) => {
|
||||
expect(error.code).toEqual(111);
|
||||
role.set('name','testRole');
|
||||
role.save(null,{useMasterKey:true})
|
||||
.then((r2)=>{
|
||||
fail("_Role object should not save without ACL.");
|
||||
}, (error2) =>{
|
||||
expect(error2.code).toEqual(111);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("Should properly resolve roles", (done) => {
|
||||
let admin = new Parse.Role("Admin", new Parse.ACL());
|
||||
let moderator = new Parse.Role("Moderator", new Parse.ACL());
|
||||
let superModerator = new Parse.Role("SuperModerator", new Parse.ACL());
|
||||
let contentManager = new Parse.Role('ContentManager', new Parse.ACL());
|
||||
let superContentManager = new Parse.Role('SuperContentManager', new Parse.ACL());
|
||||
Parse.Object.saveAll([admin, moderator, contentManager, superModerator, superContentManager], {useMasterKey: true}).then(() => {
|
||||
contentManager.getRoles().add([moderator, superContentManager]);
|
||||
moderator.getRoles().add([admin, superModerator]);
|
||||
superContentManager.getRoles().add(superModerator);
|
||||
return Parse.Object.saveAll([admin, moderator, contentManager, superModerator, superContentManager], {useMasterKey: true});
|
||||
}).then(() => {
|
||||
var auth = new Auth({ config: new Config("test"), isMaster: true });
|
||||
// For each role, fetch their sibling, what they inherit
|
||||
// return with result and roleId for later comparison
|
||||
let promises = [admin, moderator, contentManager, superModerator].map((role) => {
|
||||
return auth._getAllRoleNamesForId(role.id).then((result) => {
|
||||
return Parse.Promise.as({
|
||||
id: role.id,
|
||||
name: role.get('name'),
|
||||
roleIds: result
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
return Parse.Promise.when(promises);
|
||||
}).then((results) => {
|
||||
results.forEach((result) => {
|
||||
let id = result.id;
|
||||
let roleIds = result.roleIds;
|
||||
if (id == admin.id) {
|
||||
expect(roleIds.length).toBe(2);
|
||||
expect(roleIds.indexOf(moderator.id)).not.toBe(-1);
|
||||
expect(roleIds.indexOf(contentManager.id)).not.toBe(-1);
|
||||
} else if (id == moderator.id) {
|
||||
expect(roleIds.length).toBe(1);
|
||||
expect(roleIds.indexOf(contentManager.id)).toBe(0);
|
||||
} else if (id == contentManager.id) {
|
||||
expect(roleIds.length).toBe(0);
|
||||
} else if (id == superModerator.id) {
|
||||
expect(roleIds.length).toBe(3);
|
||||
expect(roleIds.indexOf(moderator.id)).not.toBe(-1);
|
||||
expect(roleIds.indexOf(contentManager.id)).not.toBe(-1);
|
||||
expect(roleIds.indexOf(superContentManager.id)).not.toBe(-1);
|
||||
}
|
||||
});
|
||||
done();
|
||||
}).fail((err) => {
|
||||
console.error(err);
|
||||
done();
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
it('can create role and query empty users', (done)=> {
|
||||
var roleACL = new Parse.ACL();
|
||||
roleACL.setPublicReadAccess(true);
|
||||
var role = new Parse.Role('subscribers', roleACL);
|
||||
role.save({}, {useMasterKey : true})
|
||||
.then((x)=>{
|
||||
var query = role.relation('users').query();
|
||||
query.find({useMasterKey : true})
|
||||
.then((users)=>{
|
||||
done();
|
||||
}, (e)=>{
|
||||
fail('should not have errors');
|
||||
done();
|
||||
});
|
||||
}, (e) => {
|
||||
console.log(e);
|
||||
fail('should not have errored');
|
||||
});
|
||||
});
|
||||
|
||||
// Based on various scenarios described in issues #827 and #683,
|
||||
it('should properly handle role permissions on objects', (done) => {
|
||||
var user, user2, user3;
|
||||
var role, role2, role3;
|
||||
var obj, obj2;
|
||||
|
||||
var prACL = new Parse.ACL();
|
||||
prACL.setPublicReadAccess(true);
|
||||
var adminACL, superACL, customerACL;
|
||||
|
||||
createTestUser().then((x) => {
|
||||
user = x;
|
||||
user2 = new Parse.User();
|
||||
return user2.save({ username: 'user2', password: 'omgbbq' });
|
||||
}).then((x) => {
|
||||
user3 = new Parse.User();
|
||||
return user3.save({ username: 'user3', password: 'omgbbq' });
|
||||
}).then((x) => {
|
||||
role = new Parse.Role('Admin', prACL);
|
||||
role.getUsers().add(user);
|
||||
return role.save({}, { useMasterKey: true });
|
||||
}).then(() => {
|
||||
adminACL = new Parse.ACL();
|
||||
adminACL.setRoleReadAccess("Admin", true);
|
||||
adminACL.setRoleWriteAccess("Admin", true);
|
||||
|
||||
role2 = new Parse.Role('Super', prACL);
|
||||
role2.getUsers().add(user2);
|
||||
return role2.save({}, { useMasterKey: true });
|
||||
}).then(() => {
|
||||
superACL = new Parse.ACL();
|
||||
superACL.setRoleReadAccess("Super", true);
|
||||
superACL.setRoleWriteAccess("Super", true);
|
||||
|
||||
role.getRoles().add(role2);
|
||||
return role.save({}, { useMasterKey: true });
|
||||
}).then(() => {
|
||||
role3 = new Parse.Role('Customer', prACL);
|
||||
role3.getUsers().add(user3);
|
||||
role3.getRoles().add(role);
|
||||
return role3.save({}, { useMasterKey: true });
|
||||
}).then(() => {
|
||||
customerACL = new Parse.ACL();
|
||||
customerACL.setRoleReadAccess("Customer", true);
|
||||
customerACL.setRoleWriteAccess("Customer", true);
|
||||
|
||||
var query = new Parse.Query('_Role');
|
||||
return query.find({ useMasterKey: true });
|
||||
}).then((x) => {
|
||||
expect(x.length).toEqual(3);
|
||||
|
||||
obj = new Parse.Object('TestObjectRoles');
|
||||
obj.set('ACL', customerACL);
|
||||
return obj.save(null, { useMasterKey: true });
|
||||
}).then(() => {
|
||||
// Above, the Admin role was added to the Customer role.
|
||||
// An object secured by the Customer ACL should be able to be edited by the Admin user.
|
||||
obj.set('changedByAdmin', true);
|
||||
return obj.save(null, { sessionToken: user.getSessionToken() });
|
||||
}).then(() => {
|
||||
obj2 = new Parse.Object('TestObjectRoles');
|
||||
obj2.set('ACL', adminACL);
|
||||
return obj2.save(null, { useMasterKey: true });
|
||||
}, (e) => {
|
||||
fail('Admin user should have been able to save.');
|
||||
done();
|
||||
}).then(() => {
|
||||
// An object secured by the Admin ACL should not be able to be edited by a Customer role user.
|
||||
obj2.set('changedByCustomer', true);
|
||||
return obj2.save(null, { sessionToken: user3.getSessionToken() });
|
||||
}).then(() => {
|
||||
fail('Customer user should not have been able to save.');
|
||||
done();
|
||||
}, (e) => {
|
||||
expect(e.code).toEqual(101);
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
+534
-6
@@ -5,8 +5,11 @@
|
||||
// Tests that involve revocable sessions.
|
||||
// Tests that involve sending password reset emails.
|
||||
|
||||
"use strict";
|
||||
|
||||
var request = require('request');
|
||||
var passwordCrypto = require('../src/password');
|
||||
var Config = require('../src/Config');
|
||||
|
||||
function verifyACL(user) {
|
||||
const ACL = user.getACL();
|
||||
@@ -23,6 +26,7 @@ function verifyACL(user) {
|
||||
}
|
||||
|
||||
describe('Parse.User testing', () => {
|
||||
|
||||
it("user sign up class method", (done) => {
|
||||
Parse.User.signUp("asdf", "zxcv", null, {
|
||||
success: function(user) {
|
||||
@@ -52,6 +56,11 @@ describe('Parse.User testing', () => {
|
||||
success: function(user) {
|
||||
Parse.User.logIn("non_existent_user", "asdf3",
|
||||
expectError(Parse.Error.OBJECT_NOT_FOUND, done));
|
||||
},
|
||||
error: function(err) {
|
||||
console.error(err);
|
||||
fail("Shit should not fail");
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -80,8 +89,6 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
|
||||
it("user login with files", (done) => {
|
||||
"use strict";
|
||||
|
||||
let file = new Parse.File("yolo.txt", [1,2,3], "text/plain");
|
||||
file.save().then((file) => {
|
||||
return Parse.User.signUp("asdf", "zxcv", { "file" : file });
|
||||
@@ -95,6 +102,33 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('become', () => {
|
||||
it('sends token back', done => {
|
||||
let user = null;
|
||||
var sessionToken = null;
|
||||
|
||||
Parse.User.signUp('Jason', 'Parse', { 'code': 'red' }).then(newUser => {
|
||||
user = newUser;
|
||||
expect(user.get('code'), 'red');
|
||||
|
||||
sessionToken = newUser.getSessionToken();
|
||||
expect(sessionToken).toBeDefined();
|
||||
|
||||
return Parse.User.become(sessionToken);
|
||||
}).then(newUser => {
|
||||
expect(newUser.id).toEqual(user.id);
|
||||
expect(newUser.get('username'), 'Jason');
|
||||
expect(newUser.get('code'), 'red');
|
||||
expect(newUser.getSessionToken()).toEqual(sessionToken);
|
||||
}).then(() => {
|
||||
done();
|
||||
}, error => {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("become", (done) => {
|
||||
var user = null;
|
||||
var sessionToken = null;
|
||||
@@ -873,6 +907,50 @@ describe('Parse.User testing', () => {
|
||||
};
|
||||
};
|
||||
|
||||
var getMockMyOauthProvider = function() {
|
||||
return {
|
||||
authData: {
|
||||
id: "12345",
|
||||
access_token: "12345",
|
||||
expiration_date: new Date().toJSON(),
|
||||
},
|
||||
shouldError: false,
|
||||
loggedOut: false,
|
||||
synchronizedUserId: null,
|
||||
synchronizedAuthToken: null,
|
||||
synchronizedExpiration: null,
|
||||
|
||||
authenticate: function(options) {
|
||||
if (this.shouldError) {
|
||||
options.error(this, "An error occurred");
|
||||
} else if (this.shouldCancel) {
|
||||
options.error(this, null);
|
||||
} else {
|
||||
options.success(this, this.authData);
|
||||
}
|
||||
},
|
||||
restoreAuthentication: function(authData) {
|
||||
if (!authData) {
|
||||
this.synchronizedUserId = null;
|
||||
this.synchronizedAuthToken = null;
|
||||
this.synchronizedExpiration = null;
|
||||
return true;
|
||||
}
|
||||
this.synchronizedUserId = authData.id;
|
||||
this.synchronizedAuthToken = authData.access_token;
|
||||
this.synchronizedExpiration = authData.expiration_date;
|
||||
return true;
|
||||
},
|
||||
getAuthType: function() {
|
||||
return "myoauth";
|
||||
},
|
||||
deauthenticate: function() {
|
||||
this.loggedOut = true;
|
||||
this.restoreAuthentication(null);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
var ExtendedUser = Parse.User.extend({
|
||||
extended: function() {
|
||||
return true;
|
||||
@@ -901,6 +979,29 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('log in with provider with files', done => {
|
||||
let provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
let file = new Parse.File("yolo.txt", [1, 2, 3], "text/plain");
|
||||
file.save().then(file => {
|
||||
let user = new Parse.User();
|
||||
user.set('file', file);
|
||||
return user._linkWith('facebook', {});
|
||||
}).then(user => {
|
||||
expect(user._isLinked("facebook")).toBeTruthy();
|
||||
return Parse.User._logInWith('facebook', {});
|
||||
}).then(user => {
|
||||
let fileAgain = user.get('file');
|
||||
expect(fileAgain.name()).toMatch(/yolo.txt$/);
|
||||
expect(fileAgain.url()).toMatch(/yolo.txt$/);
|
||||
}).then(() => {
|
||||
done();
|
||||
}, error => {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("log in with provider twice", (done) => {
|
||||
var provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
@@ -976,6 +1077,32 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("login with provider should not call beforeSave trigger", (done) => {
|
||||
var provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
Parse.User._logInWith("facebook", {
|
||||
success: function(model) {
|
||||
Parse.User.logOut();
|
||||
|
||||
Parse.Cloud.beforeSave(Parse.User, function(req, res) {
|
||||
res.error("Before save shouldn't be called on login");
|
||||
});
|
||||
|
||||
Parse.User._logInWith("facebook", {
|
||||
success: function(innerModel) {
|
||||
Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className);
|
||||
done();
|
||||
},
|
||||
error: function(model, error) {
|
||||
ok(undefined, error);
|
||||
Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className);
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("link with provider", (done) => {
|
||||
var provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
@@ -1204,6 +1331,177 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("link multiple providers", (done) => {
|
||||
var provider = getMockFacebookProvider();
|
||||
var mockProvider = getMockMyOauthProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
Parse.User._logInWith("facebook", {
|
||||
success: function(model) {
|
||||
ok(model instanceof Parse.User, "Model should be a Parse.User");
|
||||
strictEqual(Parse.User.current(), model);
|
||||
ok(model.extended(), "Should have used the subclass.");
|
||||
strictEqual(provider.authData.id, provider.synchronizedUserId);
|
||||
strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
|
||||
strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
|
||||
ok(model._isLinked("facebook"), "User should be linked to facebook");
|
||||
Parse.User._registerAuthenticationProvider(mockProvider);
|
||||
let objectId = model.id;
|
||||
model._linkWith("myoauth", {
|
||||
success: function(model) {
|
||||
expect(model.id).toEqual(objectId);
|
||||
ok(model._isLinked("facebook"), "User should be linked to facebook");
|
||||
ok(model._isLinked("myoauth"), "User should be linked to myoauth");
|
||||
done();
|
||||
},
|
||||
error: function(error) {
|
||||
console.error(error);
|
||||
fail('SHould not fail');
|
||||
done();
|
||||
}
|
||||
})
|
||||
},
|
||||
error: function(model, error) {
|
||||
ok(false, "linking should have worked");
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("link multiple providers and update token", (done) => {
|
||||
var provider = getMockFacebookProvider();
|
||||
var mockProvider = getMockMyOauthProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
Parse.User._logInWith("facebook", {
|
||||
success: function(model) {
|
||||
ok(model instanceof Parse.User, "Model should be a Parse.User");
|
||||
strictEqual(Parse.User.current(), model);
|
||||
ok(model.extended(), "Should have used the subclass.");
|
||||
strictEqual(provider.authData.id, provider.synchronizedUserId);
|
||||
strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
|
||||
strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
|
||||
ok(model._isLinked("facebook"), "User should be linked to facebook");
|
||||
Parse.User._registerAuthenticationProvider(mockProvider);
|
||||
let objectId = model.id;
|
||||
model._linkWith("myoauth", {
|
||||
success: function(model) {
|
||||
expect(model.id).toEqual(objectId);
|
||||
ok(model._isLinked("facebook"), "User should be linked to facebook");
|
||||
ok(model._isLinked("myoauth"), "User should be linked to myoauth");
|
||||
model._linkWith("facebook", {
|
||||
success: () => {
|
||||
ok(model._isLinked("facebook"), "User should be linked to facebook");
|
||||
ok(model._isLinked("myoauth"), "User should be linked to myoauth");
|
||||
done();
|
||||
},
|
||||
error: () => {
|
||||
fail('should link again');
|
||||
done();
|
||||
}
|
||||
})
|
||||
},
|
||||
error: function(error) {
|
||||
console.error(error);
|
||||
fail('SHould not fail');
|
||||
done();
|
||||
}
|
||||
})
|
||||
},
|
||||
error: function(model, error) {
|
||||
ok(false, "linking should have worked");
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail linking with existing', (done) => {
|
||||
var provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
Parse.User._logInWith("facebook", {
|
||||
success: function(model) {
|
||||
Parse.User.logOut().then(() => {
|
||||
let user = new Parse.User();
|
||||
user.setUsername('user');
|
||||
user.setPassword('password');
|
||||
return user.signUp().then(() => {
|
||||
// try to link here
|
||||
user._linkWith('facebook', {
|
||||
success: () => {
|
||||
fail('should not succeed');
|
||||
done();
|
||||
},
|
||||
error: (err) => {
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail linking with existing', (done) => {
|
||||
var provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
Parse.User._logInWith("facebook", {
|
||||
success: function(model) {
|
||||
let userId = model.id;
|
||||
Parse.User.logOut().then(() => {
|
||||
request.post({
|
||||
url:Parse.serverURL+'/classes/_User',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
},
|
||||
json: {authData: {facebook: provider.authData}}
|
||||
}, (err,res, body) => {
|
||||
// make sure the location header is properly set
|
||||
expect(userId).not.toBeUndefined();
|
||||
expect(body.objectId).toEqual(userId);
|
||||
expect(res.headers.location).toEqual(Parse.serverURL+'/users/'+userId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should have authData in beforeSave and afterSave', (done) => {
|
||||
|
||||
Parse.Cloud.beforeSave('_User', (request, response) => {
|
||||
let authData = request.object.get('authData');
|
||||
expect(authData).not.toBeUndefined();
|
||||
if (authData) {
|
||||
expect(authData.facebook.id).toEqual('8675309');
|
||||
expect(authData.facebook.access_token).toEqual('jenny');
|
||||
} else {
|
||||
fail('authData should be set');
|
||||
}
|
||||
response.success();
|
||||
});
|
||||
|
||||
Parse.Cloud.afterSave('_User', (request, response) => {
|
||||
let authData = request.object.get('authData');
|
||||
expect(authData).not.toBeUndefined();
|
||||
if (authData) {
|
||||
expect(authData.facebook.id).toEqual('8675309');
|
||||
expect(authData.facebook.access_token).toEqual('jenny');
|
||||
} else {
|
||||
fail('authData should be set');
|
||||
}
|
||||
response.success();
|
||||
});
|
||||
|
||||
var provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
Parse.User._logInWith("facebook", {
|
||||
success: function(model) {
|
||||
Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className);
|
||||
Parse.Cloud._removeHook('Triggers', 'afterSave', Parse.User.className);
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('set password then change password', (done) => {
|
||||
Parse.User.signUp('bob', 'barker').then((bob) => {
|
||||
bob.setPassword('meower');
|
||||
@@ -1471,9 +1769,37 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('user get session from token', (done) => {
|
||||
it('user get session from token on signup', (done) => {
|
||||
Parse.Promise.as().then(() => {
|
||||
return Parse.User.signUp("finn", "human", { foo: "bar" });
|
||||
}).then((user) => {
|
||||
request.get({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Session-Token': user.getSessionToken(),
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
},
|
||||
url: 'http://localhost:8378/1/sessions/me',
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(typeof b.sessionToken).toEqual('string');
|
||||
expect(typeof b.createdWith).toEqual('object');
|
||||
expect(b.createdWith.action).toEqual('signup');
|
||||
expect(typeof b.user).toEqual('object');
|
||||
expect(b.user.objectId).toEqual(user.id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('user get session from token on login', (done) => {
|
||||
Parse.Promise.as().then(() => {
|
||||
return Parse.User.signUp("finn", "human", { foo: "bar" });
|
||||
}).then((user) => {
|
||||
return Parse.User.logOut().then(() => {
|
||||
return Parse.User.logIn("finn", "human");
|
||||
})
|
||||
}).then((user) => {
|
||||
request.get({
|
||||
headers: {
|
||||
@@ -1628,7 +1954,7 @@ describe('Parse.User testing', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('test parse user become', (done) => {
|
||||
var sessionToken = null;
|
||||
Parse.Promise.as().then(function() {
|
||||
@@ -1670,7 +1996,209 @@ describe('Parse.User testing', () => {
|
||||
expect(e.code).toEqual(Parse.Error.SESSION_MISSING);
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('support user/password signup with empty authData block', (done) => {
|
||||
// The android SDK can send an empty authData object along with username and password.
|
||||
Parse.User.signUp('artof', 'thedeal', { authData: {} }).then((user) => {
|
||||
done();
|
||||
}, (error) => {
|
||||
fail('Signup should have succeeded.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("session expiresAt correct format", (done) => {
|
||||
Parse.User.signUp("asdf", "zxcv", null, {
|
||||
success: function(user) {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/classes/_Session',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
}, (error, response, body) => {
|
||||
expect(body.results[0].expiresAt.__type).toEqual('Date');
|
||||
done();
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Sometimes the authData still has null on that keys
|
||||
// https://github.com/ParsePlatform/parse-server/issues/935
|
||||
it('should cleanup null authData keys', (done) => {
|
||||
let database = new Config(Parse.applicationId).database;
|
||||
database.create('_User', {
|
||||
username: 'user',
|
||||
password: '$2a$10$8/wZJyEuiEaobBBqzTG.jeY.XSFJd0rzaN//ososvEI4yLqI.4aie',
|
||||
_auth_data_facebook: null
|
||||
}, {}).then(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/login?username=user&password=test',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
json: true
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(body);
|
||||
}
|
||||
})
|
||||
})
|
||||
}).then((user) => {
|
||||
let authData = user.authData;
|
||||
expect(user.username).toEqual('user');
|
||||
expect(authData).toBeUndefined();
|
||||
done();
|
||||
}).catch((err) => {
|
||||
fail('this should not fail');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
// https://github.com/ParsePlatform/parse-server/issues/1198
|
||||
it('should cleanup null authData keys ParseUser update', (done) => {
|
||||
Parse.Cloud.beforeSave('_User', (req, res) => {
|
||||
req.object.set('foo', 'bar');
|
||||
res.success();
|
||||
});
|
||||
|
||||
// Simulate anonymous user save
|
||||
new Promise((resolve, reject) => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/classes/_User',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
json: {authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}}
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(body);
|
||||
}
|
||||
});
|
||||
}).then((user) => {
|
||||
// Simulate registration
|
||||
return new Promise((resolve, reject) => {
|
||||
request.put({
|
||||
url: 'http://localhost:8378/1/classes/_User/' + user.objectId,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Session-Token': user.sessionToken,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
json: {
|
||||
authData: {anonymous: null},
|
||||
user: 'user',
|
||||
password: 'password',
|
||||
}
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(body);
|
||||
}
|
||||
});
|
||||
});
|
||||
}).then((user) => {
|
||||
expect(typeof user).toEqual('object');
|
||||
expect(user.authData).toBeUndefined();
|
||||
Parse.Cloud._removeHook('Triggers', 'beforeSave', '_User');
|
||||
done();
|
||||
}).catch((err) => {
|
||||
fail('no request should fail: ' + JSON.stringify(err));
|
||||
Parse.Cloud._removeHook('Triggers', 'beforeSave', '_User');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should aftersave with full object', (done) => {
|
||||
var hit = 0;
|
||||
Parse.Cloud.afterSave('_User', (req, res) => {
|
||||
hit++;
|
||||
expect(req.object.get('username')).toEqual('User');
|
||||
res.success();
|
||||
});
|
||||
let user = new Parse.User()
|
||||
user.setUsername('User');
|
||||
user.setPassword('pass');
|
||||
user.signUp().then(()=> {
|
||||
user.set('hello', 'world');
|
||||
return user.save();
|
||||
}).then(() => {
|
||||
Parse.Cloud._removeHook('Triggers', 'afterSave', '_User');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('changes to a user should update the cache', (done) => {
|
||||
Parse.Cloud.define('testUpdatedUser', (req, res) => {
|
||||
expect(req.user.get('han')).toEqual('solo');
|
||||
res.success({});
|
||||
});
|
||||
let user = new Parse.User();
|
||||
user.setUsername('harrison');
|
||||
user.setPassword('ford');
|
||||
user.signUp().then(() => {
|
||||
user.set('han', 'solo');
|
||||
return user.save();
|
||||
}).then(() => {
|
||||
return Parse.Cloud.run('testUpdatedUser');
|
||||
}).then(() => {
|
||||
done();
|
||||
}, (e) => {
|
||||
fail('Should not have failed.');
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should fail to become user with expired token', (done) => {
|
||||
Parse.User.signUp("auser", "somepass", null, {
|
||||
success: function(user) {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/classes/_Session',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
}, (error, response, body) => {
|
||||
var id = body.results[0].objectId;
|
||||
var expiresAt = new Date((new Date()).setYear(2015));
|
||||
var token = body.results[0].sessionToken;
|
||||
request.put({
|
||||
url: "http://localhost:8378/1/classes/_Session/" + id,
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
body: {
|
||||
expiresAt: { __type: "Date", iso: expiresAt.toISOString() },
|
||||
},
|
||||
}, (error, response, body) => {
|
||||
Parse.User.become(token)
|
||||
.then(() => { fail("Should not have succeded"); })
|
||||
.fail((err) => {
|
||||
expect(err.code).toEqual(209);
|
||||
expect(err.message).toEqual("Session token is expired.");
|
||||
Parse.User.logOut() // Logout to prevent polluting CLI with messages
|
||||
.then(done());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
var ParseWebSocket = require('../src/LiveQuery/ParseWebSocketServer').ParseWebSocket;
|
||||
|
||||
describe('ParseWebSocket', function() {
|
||||
|
||||
it('can be initialized', function() {
|
||||
var ws = {};
|
||||
var parseWebSocket = new ParseWebSocket(ws);
|
||||
|
||||
expect(parseWebSocket.ws).toBe(ws);
|
||||
});
|
||||
|
||||
it('can handle events defined in typeMap', function() {
|
||||
var ws = {
|
||||
on: jasmine.createSpy('on')
|
||||
};
|
||||
var callback = {};
|
||||
var parseWebSocket = new ParseWebSocket(ws);
|
||||
parseWebSocket.on('disconnect', callback);
|
||||
|
||||
expect(parseWebSocket.ws.on).toHaveBeenCalledWith('close', callback);
|
||||
});
|
||||
|
||||
it('can handle events which are not defined in typeMap', function() {
|
||||
var ws = {
|
||||
on: jasmine.createSpy('on')
|
||||
};
|
||||
var callback = {};
|
||||
var parseWebSocket = new ParseWebSocket(ws);
|
||||
parseWebSocket.on('open', callback);
|
||||
|
||||
expect(parseWebSocket.ws.on).toHaveBeenCalledWith('open', callback);
|
||||
});
|
||||
|
||||
it('can send a message', function() {
|
||||
var ws = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
var parseWebSocket = new ParseWebSocket(ws);
|
||||
parseWebSocket.send('message')
|
||||
|
||||
expect(parseWebSocket.ws.send).toHaveBeenCalledWith('message');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
var ParseWebSocketServer = require('../src/LiveQuery/ParseWebSocketServer').ParseWebSocketServer;
|
||||
|
||||
describe('ParseWebSocketServer', function() {
|
||||
|
||||
beforeEach(function(done) {
|
||||
// Mock ws server
|
||||
var EventEmitter = require('events');
|
||||
var mockServer = function() {
|
||||
return new EventEmitter();
|
||||
};
|
||||
jasmine.mockLibrary('ws', 'Server', mockServer);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can handle connect event when ws is open', function(done) {
|
||||
var onConnectCallback = jasmine.createSpy('onConnectCallback');
|
||||
var parseWebSocketServer = new ParseWebSocketServer({}, onConnectCallback, 5).server;
|
||||
var ws = {
|
||||
readyState: 0,
|
||||
OPEN: 0,
|
||||
ping: jasmine.createSpy('ping')
|
||||
};
|
||||
parseWebSocketServer.emit('connection', ws);
|
||||
|
||||
// Make sure callback is called
|
||||
expect(onConnectCallback).toHaveBeenCalled();
|
||||
// Make sure we ping to the client
|
||||
setTimeout(function() {
|
||||
expect(ws.ping).toHaveBeenCalled();
|
||||
done();
|
||||
}, 10)
|
||||
});
|
||||
|
||||
afterEach(function(){
|
||||
jasmine.restoreLibrary('ws', 'Server');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
var PromiseRouter = require("../src/PromiseRouter").default;
|
||||
|
||||
describe("PromiseRouter", () => {
|
||||
|
||||
it("should properly handle rejects", (done) => {
|
||||
var router = new PromiseRouter();
|
||||
router.route("GET", "/dummy", (req)=> {
|
||||
return Promise.reject({
|
||||
error: "an error",
|
||||
code: -1
|
||||
})
|
||||
}, (req) => {
|
||||
fail("this should not be called");
|
||||
});
|
||||
|
||||
router.routes[0].handler({}).then((result) => {
|
||||
console.error(result);
|
||||
fail("this should not be called");
|
||||
done();
|
||||
}, (error)=> {
|
||||
expect(error.error).toEqual("an error");
|
||||
expect(error.code).toEqual(-1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
})
|
||||
@@ -0,0 +1,86 @@
|
||||
|
||||
var request = require('request');
|
||||
|
||||
describe("public API", () => {
|
||||
beforeEach(done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
});
|
||||
done();
|
||||
})
|
||||
it("should get invalid_link.html", (done) => {
|
||||
request('http://localhost:8378/1/apps/invalid_link.html', (err, httpResponse, body) => {
|
||||
expect(httpResponse.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should get choose_password", (done) => {
|
||||
request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse, body) => {
|
||||
expect(httpResponse.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should get verify_email_success.html", (done) => {
|
||||
request('http://localhost:8378/1/apps/verify_email_success.html', (err, httpResponse, body) => {
|
||||
expect(httpResponse.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should get password_reset_success.html", (done) => {
|
||||
request('http://localhost:8378/1/apps/password_reset_success.html', (err, httpResponse, body) => {
|
||||
expect(httpResponse.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("public API without publicServerURL", () => {
|
||||
beforeEach(done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
});
|
||||
done();
|
||||
})
|
||||
it("should get 404 on verify_email", (done) => {
|
||||
request('http://localhost:8378/1/apps/test/verify_email', (err, httpResponse, body) => {
|
||||
expect(httpResponse.statusCode).toBe(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should get 404 choose_password", (done) => {
|
||||
request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse, body) => {
|
||||
expect(httpResponse.statusCode).toBe(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should get 404 on request_password_reset", (done) => {
|
||||
request('http://localhost:8378/1/apps/test/request_password_reset', (err, httpResponse, body) => {
|
||||
expect(httpResponse.statusCode).toBe(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,209 @@
|
||||
var request = require("request");
|
||||
|
||||
|
||||
|
||||
function createProduct() {
|
||||
const file = new Parse.File("name", {
|
||||
base64: new Buffer("download_file", "utf-8").toString("base64")
|
||||
}, "text");
|
||||
return file.save().then(function(){
|
||||
var product = new Parse.Object("_Product");
|
||||
product.set({
|
||||
download: file,
|
||||
icon: file,
|
||||
title: "a product",
|
||||
subtitle: "a product",
|
||||
order: 1,
|
||||
productIdentifier: "a-product"
|
||||
})
|
||||
return product.save();
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
describe("test validate_receipt endpoint", () => {
|
||||
|
||||
beforeEach( done => {
|
||||
createProduct().then(done).fail(function(err){
|
||||
console.error(err);
|
||||
done();
|
||||
})
|
||||
})
|
||||
|
||||
it("should bypass appstore validation", (done) => {
|
||||
|
||||
request.post({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'},
|
||||
url: 'http://localhost:8378/1/validate_purchase',
|
||||
json: true,
|
||||
body: {
|
||||
productIdentifier: "a-product",
|
||||
receipt: {
|
||||
__type: "Bytes",
|
||||
base64: new Buffer("receipt", "utf-8").toString("base64")
|
||||
},
|
||||
bypassAppStoreValidation: true
|
||||
}
|
||||
}, function(err, res, body){
|
||||
if (typeof body != "object") {
|
||||
fail("Body is not an object");
|
||||
done();
|
||||
} else {
|
||||
expect(body.__type).toEqual("File");
|
||||
const url = body.url;
|
||||
request.get({
|
||||
url: url
|
||||
}, function(err, res, body) {
|
||||
expect(body).toEqual("download_file");
|
||||
done();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail for missing receipt", (done) => {
|
||||
request.post({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'},
|
||||
url: 'http://localhost:8378/1/validate_purchase',
|
||||
json: true,
|
||||
body: {
|
||||
productIdentifier: "a-product",
|
||||
bypassAppStoreValidation: true
|
||||
}
|
||||
}, function(err, res, body){
|
||||
if (typeof body != "object") {
|
||||
fail("Body is not an object");
|
||||
done();
|
||||
} else {
|
||||
expect(body.code).toEqual(Parse.Error.INVALID_JSON);
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail for missing product identifier", (done) => {
|
||||
request.post({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'},
|
||||
url: 'http://localhost:8378/1/validate_purchase',
|
||||
json: true,
|
||||
body: {
|
||||
receipt: {
|
||||
__type: "Bytes",
|
||||
base64: new Buffer("receipt", "utf-8").toString("base64")
|
||||
},
|
||||
bypassAppStoreValidation: true
|
||||
}
|
||||
}, function(err, res, body){
|
||||
if (typeof body != "object") {
|
||||
fail("Body is not an object");
|
||||
done();
|
||||
} else {
|
||||
expect(body.code).toEqual(Parse.Error.INVALID_JSON);
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should bypass appstore validation and not find product", (done) => {
|
||||
|
||||
request.post({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'},
|
||||
url: 'http://localhost:8378/1/validate_purchase',
|
||||
json: true,
|
||||
body: {
|
||||
productIdentifier: "another-product",
|
||||
receipt: {
|
||||
__type: "Bytes",
|
||||
base64: new Buffer("receipt", "utf-8").toString("base64")
|
||||
},
|
||||
bypassAppStoreValidation: true
|
||||
}
|
||||
}, function(err, res, body){
|
||||
if (typeof body != "object") {
|
||||
fail("Body is not an object");
|
||||
done();
|
||||
} else {
|
||||
expect(body.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
|
||||
expect(body.error).toEqual('Object not found.');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail at appstore validation", (done) => {
|
||||
|
||||
request.post({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'},
|
||||
url: 'http://localhost:8378/1/validate_purchase',
|
||||
json: true,
|
||||
body: {
|
||||
productIdentifier: "a-product",
|
||||
receipt: {
|
||||
__type: "Bytes",
|
||||
base64: new Buffer("receipt", "utf-8").toString("base64")
|
||||
},
|
||||
}
|
||||
}, function(err, res, body){
|
||||
if (typeof body != "object") {
|
||||
fail("Body is not an object");
|
||||
} else {
|
||||
expect(body.status).toBe(21002);
|
||||
expect(body.error).toBe('The data in the receipt-data property was malformed or missing.');
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not create a _Product", (done) => {
|
||||
var product = new Parse.Object("_Product");
|
||||
product.save().then(function(){
|
||||
fail("Should not be able to save");
|
||||
done();
|
||||
}, function(err){
|
||||
expect(err.code).toEqual(Parse.Error.INCORRECT_TYPE);
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should be able to update a _Product", (done) => {
|
||||
var query = new Parse.Query("_Product");
|
||||
query.first().then(function(product){
|
||||
product.set("title", "a new title");
|
||||
return product.save();
|
||||
}).then(function(productAgain){
|
||||
expect(productAgain.get('downloadName')).toEqual(productAgain.get('download').name());
|
||||
expect(productAgain.get("title")).toEqual("a new title");
|
||||
done();
|
||||
}).fail(function(err){
|
||||
fail(JSON.stringify(err));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not be able to remove a require key in a _Product", (done) => {
|
||||
var query = new Parse.Query("_Product");
|
||||
query.first().then(function(product){
|
||||
product.unset("title");
|
||||
return product.save();
|
||||
}).then(function(productAgain){
|
||||
fail("Should not succeed");
|
||||
done();
|
||||
}).fail(function(err){
|
||||
expect(err.code).toEqual(Parse.Error.INCORRECT_TYPE);
|
||||
expect(err.message).toEqual("title is required.");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
+307
-115
@@ -1,106 +1,33 @@
|
||||
"use strict";
|
||||
var PushController = require('../src/Controllers/PushController').PushController;
|
||||
var pushStatusHandler = require('../src/pushStatusHandler');
|
||||
var Config = require('../src/Config');
|
||||
|
||||
const successfulTransmissions = function(body, installations) {
|
||||
|
||||
let promises = installations.map((device) => {
|
||||
return Promise.resolve({
|
||||
transmitted: true,
|
||||
device: device,
|
||||
})
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
const successfulIOS = function(body, installations) {
|
||||
|
||||
let promises = installations.map((device) => {
|
||||
return Promise.resolve({
|
||||
transmitted: device.deviceType == "ios",
|
||||
device: device,
|
||||
})
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
describe('PushController', () => {
|
||||
it('can check valid master key of request', (done) => {
|
||||
// Make mock request
|
||||
var request = {
|
||||
info: {
|
||||
masterKey: 'masterKey'
|
||||
},
|
||||
config: {
|
||||
masterKey: 'masterKey'
|
||||
}
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
PushController.validateMasterKey(request);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can check invalid master key of request', (done) => {
|
||||
// Make mock request
|
||||
var request = {
|
||||
info: {
|
||||
masterKey: 'masterKey'
|
||||
},
|
||||
config: {
|
||||
masterKey: 'masterKeyAgain'
|
||||
}
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
PushController.validateMasterKey(request);
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can get query condition when channels is set', (done) => {
|
||||
// Make mock request
|
||||
var request = {
|
||||
body: {
|
||||
channels: ['Giants', 'Mets']
|
||||
}
|
||||
}
|
||||
|
||||
var where = PushController.getQueryCondition(request);
|
||||
expect(where).toEqual({
|
||||
'channels': {
|
||||
'$in': ['Giants', 'Mets']
|
||||
}
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
it('can get query condition when where is set', (done) => {
|
||||
// Make mock request
|
||||
var request = {
|
||||
body: {
|
||||
'where': {
|
||||
'injuryReports': true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var where = PushController.getQueryCondition(request);
|
||||
expect(where).toEqual({
|
||||
'injuryReports': true
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
it('can get query condition when nothing is set', (done) => {
|
||||
// Make mock request
|
||||
var request = {
|
||||
body: {
|
||||
}
|
||||
}
|
||||
|
||||
expect(function() {
|
||||
PushController.getQueryCondition(request);
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can throw on getQueryCondition when channels and where are set', (done) => {
|
||||
// Make mock request
|
||||
var request = {
|
||||
body: {
|
||||
'channels': {
|
||||
'$in': ['Giants', 'Mets']
|
||||
},
|
||||
'where': {
|
||||
'injuryReports': true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(function() {
|
||||
PushController.getQueryCondition(request);
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can validate device type when no device type is set', (done) => {
|
||||
// Make query condition
|
||||
var where = {
|
||||
@@ -170,13 +97,11 @@ describe('PushController', () => {
|
||||
it('can get expiration time in string format', (done) => {
|
||||
// Make mock request
|
||||
var timeStr = '2015-03-19T22:05:08Z';
|
||||
var request = {
|
||||
body: {
|
||||
var body = {
|
||||
'expiration_time': timeStr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var time = PushController.getExpirationTime(request);
|
||||
var time = PushController.getExpirationTime(body);
|
||||
expect(time).toEqual(new Date(timeStr).valueOf());
|
||||
done();
|
||||
});
|
||||
@@ -184,28 +109,295 @@ describe('PushController', () => {
|
||||
it('can get expiration time in number format', (done) => {
|
||||
// Make mock request
|
||||
var timeNumber = 1426802708;
|
||||
var request = {
|
||||
body: {
|
||||
'expiration_time': timeNumber
|
||||
}
|
||||
var body = {
|
||||
'expiration_time': timeNumber
|
||||
}
|
||||
|
||||
var time = PushController.getExpirationTime(request);
|
||||
var time = PushController.getExpirationTime(body);
|
||||
expect(time).toEqual(timeNumber * 1000);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can throw on getExpirationTime in invalid format', (done) => {
|
||||
// Make mock request
|
||||
var request = {
|
||||
body: {
|
||||
'expiration_time': 'abcd'
|
||||
}
|
||||
var body = {
|
||||
'expiration_time': 'abcd'
|
||||
}
|
||||
|
||||
expect(function(){
|
||||
PushController.getExpirationTime(request);
|
||||
PushController.getExpirationTime(body);
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('properly increment badges', (done) => {
|
||||
|
||||
var payload = {data:{
|
||||
alert: "Hello World!",
|
||||
badge: "Increment",
|
||||
}}
|
||||
var installations = [];
|
||||
while(installations.length != 10) {
|
||||
var installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_"+installations.length);
|
||||
installation.set("deviceToken","device_token_"+installations.length)
|
||||
installation.set("badge", installations.length);
|
||||
installation.set("originalBadge", installations.length);
|
||||
installation.set("deviceType", "ios");
|
||||
installations.push(installation);
|
||||
}
|
||||
|
||||
while(installations.length != 15) {
|
||||
var installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_"+installations.length);
|
||||
installation.set("deviceToken","device_token_"+installations.length)
|
||||
installation.set("deviceType", "android");
|
||||
installations.push(installation);
|
||||
}
|
||||
|
||||
var pushAdapter = {
|
||||
send: function(body, installations) {
|
||||
var badge = body.data.badge;
|
||||
installations.forEach((installation) => {
|
||||
if (installation.deviceType == "ios") {
|
||||
expect(installation.badge).toEqual(badge);
|
||||
expect(installation.originalBadge+1).toEqual(installation.badge);
|
||||
} else {
|
||||
expect(installation.badge).toBeUndefined();
|
||||
}
|
||||
})
|
||||
return successfulTransmissions(body, installations);
|
||||
},
|
||||
getValidPushTypes: function() {
|
||||
return ["ios", "android"];
|
||||
}
|
||||
}
|
||||
|
||||
var config = new Config(Parse.applicationId);
|
||||
var auth = {
|
||||
isMaster: true
|
||||
}
|
||||
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId);
|
||||
Parse.Object.saveAll(installations).then((installations) => {
|
||||
return pushController.sendPush(payload, {}, config, auth);
|
||||
}).then((result) => {
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("should not fail");
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('properly set badges to 1', (done) => {
|
||||
|
||||
var payload = {data: {
|
||||
alert: "Hello World!",
|
||||
badge: 1,
|
||||
}}
|
||||
var installations = [];
|
||||
while(installations.length != 10) {
|
||||
var installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_"+installations.length);
|
||||
installation.set("deviceToken","device_token_"+installations.length)
|
||||
installation.set("badge", installations.length);
|
||||
installation.set("originalBadge", installations.length);
|
||||
installation.set("deviceType", "ios");
|
||||
installations.push(installation);
|
||||
}
|
||||
|
||||
var pushAdapter = {
|
||||
send: function(body, installations) {
|
||||
var badge = body.data.badge;
|
||||
installations.forEach((installation) => {
|
||||
expect(installation.badge).toEqual(badge);
|
||||
expect(1).toEqual(installation.badge);
|
||||
})
|
||||
return successfulTransmissions(body, installations);
|
||||
},
|
||||
getValidPushTypes: function() {
|
||||
return ["ios"];
|
||||
}
|
||||
}
|
||||
|
||||
var config = new Config(Parse.applicationId);
|
||||
var auth = {
|
||||
isMaster: true
|
||||
}
|
||||
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId);
|
||||
Parse.Object.saveAll(installations).then((installations) => {
|
||||
return pushController.sendPush(payload, {}, config, auth);
|
||||
}).then((result) => {
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("should not fail");
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('properly creates _PushStatus', (done) => {
|
||||
|
||||
var installations = [];
|
||||
while(installations.length != 10) {
|
||||
var installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_"+installations.length);
|
||||
installation.set("deviceToken","device_token_"+installations.length)
|
||||
installation.set("badge", installations.length);
|
||||
installation.set("originalBadge", installations.length);
|
||||
installation.set("deviceType", "ios");
|
||||
installations.push(installation);
|
||||
}
|
||||
|
||||
while(installations.length != 15) {
|
||||
var installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_"+installations.length);
|
||||
installation.set("deviceToken","device_token_"+installations.length)
|
||||
installation.set("deviceType", "android");
|
||||
installations.push(installation);
|
||||
}
|
||||
var payload = {data: {
|
||||
alert: "Hello World!",
|
||||
badge: 1,
|
||||
}}
|
||||
|
||||
var pushAdapter = {
|
||||
send: function(body, installations) {
|
||||
return successfulIOS(body, installations);
|
||||
},
|
||||
getValidPushTypes: function() {
|
||||
return ["ios"];
|
||||
}
|
||||
}
|
||||
|
||||
var config = new Config(Parse.applicationId);
|
||||
var auth = {
|
||||
isMaster: true
|
||||
}
|
||||
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId);
|
||||
Parse.Object.saveAll(installations).then(() => {
|
||||
return pushController.sendPush(payload, {}, config, auth);
|
||||
}).then((result) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000);
|
||||
});
|
||||
}).then(() => {
|
||||
let query = new Parse.Query('_PushStatus');
|
||||
return query.find({useMasterKey: true});
|
||||
}).then((results) => {
|
||||
expect(results.length).toBe(1);
|
||||
let result = results[0];
|
||||
expect(result.createdAt instanceof Date).toBe(true);
|
||||
expect(result.get('source')).toEqual('rest');
|
||||
expect(result.get('query')).toEqual(JSON.stringify({}));
|
||||
expect(result.get('payload')).toEqual(payload.data);
|
||||
expect(result.get('status')).toEqual('succeeded');
|
||||
expect(result.get('numSent')).toEqual(10);
|
||||
expect(result.get('sentPerType')).toEqual({
|
||||
'ios': 10 // 10 ios
|
||||
});
|
||||
expect(result.get('numFailed')).toEqual(5);
|
||||
expect(result.get('failedPerType')).toEqual({
|
||||
'android': 5 // android
|
||||
});
|
||||
// Try to get it without masterKey
|
||||
let query = new Parse.Query('_PushStatus');
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
expect(results.length).toBe(0);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should properly report failures in _PushStatus', (done) => {
|
||||
var pushAdapter = {
|
||||
send: function(body, installations) {
|
||||
return installations.map((installation) => {
|
||||
return Promise.resolve({
|
||||
deviceType: installation.deviceType
|
||||
})
|
||||
})
|
||||
},
|
||||
getValidPushTypes: function() {
|
||||
return ["ios"];
|
||||
}
|
||||
}
|
||||
let where = { 'channels': {
|
||||
'$ins': ['Giants', 'Mets']
|
||||
}};
|
||||
var payload = {data: {
|
||||
alert: "Hello World!",
|
||||
badge: 1,
|
||||
}}
|
||||
var config = new Config(Parse.applicationId);
|
||||
var auth = {
|
||||
isMaster: true
|
||||
}
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId);
|
||||
pushController.sendPush(payload, where, config, auth).then(() => {
|
||||
fail('should not succeed');
|
||||
done();
|
||||
}).catch(() => {
|
||||
let query = new Parse.Query('_PushStatus');
|
||||
query.find({useMasterKey: true}).then((results) => {
|
||||
expect(results.length).toBe(1);
|
||||
let pushStatus = results[0];
|
||||
expect(pushStatus.get('status')).toBe('failed');
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('should support full RESTQuery for increment', (done) => {
|
||||
var payload = {data: {
|
||||
alert: "Hello World!",
|
||||
badge: 'Increment',
|
||||
}}
|
||||
|
||||
var pushAdapter = {
|
||||
send: function(body, installations) {
|
||||
return successfulTransmissions(body, installations);
|
||||
},
|
||||
getValidPushTypes: function() {
|
||||
return ["ios"];
|
||||
}
|
||||
}
|
||||
|
||||
var config = new Config(Parse.applicationId);
|
||||
var auth = {
|
||||
isMaster: true
|
||||
}
|
||||
|
||||
let where = {
|
||||
'deviceToken': {
|
||||
'$inQuery': {
|
||||
'where': {
|
||||
'deviceType': 'ios'
|
||||
},
|
||||
className: '_Installation'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId);
|
||||
pushController.sendPush(payload, where, config, auth).then((result) => {
|
||||
done();
|
||||
}).catch((err) => {
|
||||
fail('should not fail');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should flatten', () => {
|
||||
var res = pushStatusHandler.flatten([1, [2], [[3, 4], 5], [[[6]]]])
|
||||
expect(res).toEqual([1,2,3,4,5,6]);
|
||||
})
|
||||
});
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
var PushRouter = require('../src/Routers/PushRouter').PushRouter;
|
||||
var request = require('request');
|
||||
|
||||
describe('PushRouter', () => {
|
||||
it('can get query condition when channels is set', (done) => {
|
||||
// Make mock request
|
||||
var request = {
|
||||
body: {
|
||||
channels: ['Giants', 'Mets']
|
||||
}
|
||||
}
|
||||
|
||||
var where = PushRouter.getQueryCondition(request);
|
||||
expect(where).toEqual({
|
||||
'channels': {
|
||||
'$in': ['Giants', 'Mets']
|
||||
}
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
it('can get query condition when where is set', (done) => {
|
||||
// Make mock request
|
||||
var request = {
|
||||
body: {
|
||||
'where': {
|
||||
'injuryReports': true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var where = PushRouter.getQueryCondition(request);
|
||||
expect(where).toEqual({
|
||||
'injuryReports': true
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
it('can get query condition when nothing is set', (done) => {
|
||||
// Make mock request
|
||||
var request = {
|
||||
body: {
|
||||
}
|
||||
}
|
||||
|
||||
expect(function() {
|
||||
PushRouter.getQueryCondition(request);
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can throw on getQueryCondition when channels and where are set', (done) => {
|
||||
// Make mock request
|
||||
var request = {
|
||||
body: {
|
||||
'channels': {
|
||||
'$in': ['Giants', 'Mets']
|
||||
},
|
||||
'where': {
|
||||
'injuryReports': true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(function() {
|
||||
PushRouter.getQueryCondition(request);
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('sends a push through REST', (done) => {
|
||||
request.post({
|
||||
url: Parse.serverURL+"/push",
|
||||
json: true,
|
||||
body: {
|
||||
'channels': {
|
||||
'$in': ['Giants', 'Mets']
|
||||
}
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Master-Key': Parse.masterKey
|
||||
}
|
||||
}, function(err, res, body){
|
||||
expect(body.result).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,398 @@
|
||||
var Parse = require('parse/node');
|
||||
|
||||
var Id = require('../src/LiveQuery/Id');
|
||||
var QueryTools = require('../src/LiveQuery/QueryTools');
|
||||
var queryHash = QueryTools.queryHash;
|
||||
var matchesQuery = QueryTools.matchesQuery;
|
||||
|
||||
var Item = Parse.Object.extend('Item');
|
||||
|
||||
describe('queryHash', function() {
|
||||
|
||||
it('should always hash a query to the same string', function() {
|
||||
var q = new Parse.Query(Item);
|
||||
q.equalTo('field', 'value');
|
||||
q.exists('name');
|
||||
q.ascending('createdAt');
|
||||
q.limit(10);
|
||||
var firstHash = queryHash(q);
|
||||
var secondHash = queryHash(q);
|
||||
expect(firstHash).toBe(secondHash);
|
||||
});
|
||||
|
||||
it('should return equivalent hashes for equivalent queries', function() {
|
||||
var q1 = new Parse.Query(Item);
|
||||
q1.equalTo('field', 'value');
|
||||
q1.exists('name');
|
||||
q1.lessThan('age', 30);
|
||||
q1.greaterThan('age', 3);
|
||||
q1.ascending('createdAt');
|
||||
q1.include(['name', 'age']);
|
||||
q1.limit(10);
|
||||
|
||||
var q2 = new Parse.Query(Item);
|
||||
q2.limit(10);
|
||||
q2.greaterThan('age', 3);
|
||||
q2.lessThan('age', 30);
|
||||
q2.include(['name', 'age']);
|
||||
q2.ascending('createdAt');
|
||||
q2.exists('name');
|
||||
q2.equalTo('field', 'value');
|
||||
|
||||
var firstHash = queryHash(q1);
|
||||
var secondHash = queryHash(q2);
|
||||
expect(firstHash).toBe(secondHash);
|
||||
|
||||
q1.containedIn('fruit', ['apple', 'banana', 'cherry']);
|
||||
firstHash = queryHash(q1);
|
||||
expect(firstHash).not.toBe(secondHash);
|
||||
|
||||
q2.containedIn('fruit', ['banana', 'cherry', 'apple']);
|
||||
secondHash = queryHash(q2);
|
||||
expect(secondHash).toBe(firstHash);
|
||||
|
||||
q1.containedIn('fruit', ['coconut']);
|
||||
firstHash = queryHash(q1);
|
||||
expect(firstHash).not.toBe(secondHash);
|
||||
|
||||
q1 = new Parse.Query(Item);
|
||||
q1.equalTo('field', 'value');
|
||||
q1.lessThan('age', 30);
|
||||
q1.exists('name');
|
||||
|
||||
q2 = new Parse.Query(Item);
|
||||
q2.equalTo('name', 'person');
|
||||
q2.equalTo('field', 'other');
|
||||
|
||||
firstHash = queryHash(Parse.Query.or(q1, q2));
|
||||
secondHash = queryHash(Parse.Query.or(q2, q1));
|
||||
expect(firstHash).toBe(secondHash);
|
||||
});
|
||||
|
||||
it('should not let fields of different types appear similar', function() {
|
||||
var q1 = new Parse.Query(Item);
|
||||
q1.lessThan('age', 30);
|
||||
|
||||
var q2 = new Parse.Query(Item);
|
||||
q2.equalTo('age', '{$lt:30}');
|
||||
|
||||
expect(queryHash(q1)).not.toBe(queryHash(q2));
|
||||
|
||||
q1 = new Parse.Query(Item);
|
||||
q1.equalTo('age', 15);
|
||||
|
||||
q2.equalTo('age', '15');
|
||||
|
||||
expect(queryHash(q1)).not.toBe(queryHash(q2));
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchesQuery', function() {
|
||||
it('matches blanket queries', function() {
|
||||
var obj = {
|
||||
id: new Id('Klass', 'O1'),
|
||||
value: 12
|
||||
};
|
||||
var q = new Parse.Query('Klass');
|
||||
expect(matchesQuery(obj, q)).toBe(true);
|
||||
|
||||
obj.id = new Id('Other', 'O1');
|
||||
expect(matchesQuery(obj, q)).toBe(false);
|
||||
});
|
||||
|
||||
it('matches existence queries', function() {
|
||||
var obj = {
|
||||
id: new Id('Item', 'O1'),
|
||||
count: 15
|
||||
};
|
||||
var q = new Parse.Query('Item');
|
||||
q.exists('count');
|
||||
expect(matchesQuery(obj, q)).toBe(true);
|
||||
q.exists('name');
|
||||
expect(matchesQuery(obj, q)).toBe(false);
|
||||
});
|
||||
|
||||
it('matches queries with doesNotExist constraint', function() {
|
||||
var obj = {
|
||||
id: new Id('Item', 'O1'),
|
||||
count: 15
|
||||
};
|
||||
var q = new Parse.Query('Item');
|
||||
q.doesNotExist('name');
|
||||
expect(matchesQuery(obj, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Item');
|
||||
q.doesNotExist('count');
|
||||
expect(matchesQuery(obj, q)).toBe(false);
|
||||
});
|
||||
|
||||
it('matches on equality queries', function() {
|
||||
var day = new Date();
|
||||
var location = new Parse.GeoPoint({
|
||||
latitude: 37.484815,
|
||||
longitude: -122.148377
|
||||
});
|
||||
var obj = {
|
||||
id: new Id('Person', 'O1'),
|
||||
score: 12,
|
||||
name: 'Bill',
|
||||
birthday: day,
|
||||
lastLocation: location
|
||||
};
|
||||
|
||||
var q = new Parse.Query('Person');
|
||||
q.equalTo('score', 12);
|
||||
expect(matchesQuery(obj, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Person');
|
||||
q.equalTo('name', 'Bill');
|
||||
expect(matchesQuery(obj, q)).toBe(true);
|
||||
q.equalTo('name', 'Jeff');
|
||||
expect(matchesQuery(obj, q)).toBe(false);
|
||||
|
||||
q = new Parse.Query('Person');
|
||||
q.containedIn('name', ['Adam', 'Ben', 'Charles']);
|
||||
expect(matchesQuery(obj, q)).toBe(false);
|
||||
q.containedIn('name', ['Adam', 'Bill', 'Charles']);
|
||||
expect(matchesQuery(obj, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Person');
|
||||
q.notContainedIn('name', ['Adam', 'Bill', 'Charles']);
|
||||
expect(matchesQuery(obj, q)).toBe(false);
|
||||
q.notContainedIn('name', ['Adam', 'Ben', 'Charles']);
|
||||
expect(matchesQuery(obj, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Person');
|
||||
q.equalTo('birthday', day);
|
||||
expect(matchesQuery(obj, q)).toBe(true);
|
||||
q.equalTo('birthday', new Date(1990, 1));
|
||||
expect(matchesQuery(obj, q)).toBe(false);
|
||||
|
||||
q = new Parse.Query('Person');
|
||||
q.equalTo('lastLocation', new Parse.GeoPoint({
|
||||
latitude: 37.484815,
|
||||
longitude: -122.148377
|
||||
}));
|
||||
expect(matchesQuery(obj, q)).toBe(true);
|
||||
q.equalTo('lastLocation', new Parse.GeoPoint({
|
||||
latitude: 37.4848,
|
||||
longitude: -122.1483
|
||||
}));
|
||||
expect(matchesQuery(obj, q)).toBe(false);
|
||||
|
||||
q.equalTo('lastLocation', new Parse.GeoPoint({
|
||||
latitude: 37.484815,
|
||||
longitude: -122.148377
|
||||
}));
|
||||
q.equalTo('score', 12);
|
||||
q.equalTo('name', 'Bill');
|
||||
q.equalTo('birthday', day);
|
||||
expect(matchesQuery(obj, q)).toBe(true);
|
||||
|
||||
q.equalTo('name', 'bill');
|
||||
expect(matchesQuery(obj, q)).toBe(false);
|
||||
|
||||
var img = {
|
||||
id: new Id('Image', 'I1'),
|
||||
tags: ['nofilter', 'latergram', 'tbt']
|
||||
};
|
||||
|
||||
q = new Parse.Query('Image');
|
||||
q.equalTo('tags', 'selfie');
|
||||
expect(matchesQuery(img, q)).toBe(false);
|
||||
q.equalTo('tags', 'tbt');
|
||||
expect(matchesQuery(img, q)).toBe(true);
|
||||
|
||||
var q2 = new Parse.Query('Image');
|
||||
q2.containsAll('tags', ['latergram', 'nofilter']);
|
||||
expect(matchesQuery(img, q2)).toBe(true);
|
||||
q2.containsAll('tags', ['latergram', 'selfie']);
|
||||
expect(matchesQuery(img, q2)).toBe(false);
|
||||
|
||||
var u = new Parse.User();
|
||||
u.id = 'U2';
|
||||
q = new Parse.Query('Image');
|
||||
q.equalTo('owner', u);
|
||||
|
||||
img = {
|
||||
className: 'Image',
|
||||
objectId: 'I1',
|
||||
owner: {
|
||||
className: '_User',
|
||||
objectId: 'U2'
|
||||
}
|
||||
};
|
||||
expect(matchesQuery(img, q)).toBe(true);
|
||||
|
||||
img.owner.objectId = 'U3';
|
||||
expect(matchesQuery(img, q)).toBe(false);
|
||||
});
|
||||
|
||||
it('matches on inequalities', function() {
|
||||
var player = {
|
||||
id: new Id('Person', 'O1'),
|
||||
score: 12,
|
||||
name: 'Bill',
|
||||
birthday: new Date(1980, 2, 4),
|
||||
};
|
||||
var q = new Parse.Query('Person');
|
||||
q.lessThan('score', 15);
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
q.lessThan('score', 10);
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
|
||||
q = new Parse.Query('Person');
|
||||
q.lessThanOrEqualTo('score', 15);
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
q.lessThanOrEqualTo('score', 12);
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
q.lessThanOrEqualTo('score', 10);
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
|
||||
q = new Parse.Query('Person');
|
||||
q.greaterThan('score', 15);
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
q.greaterThan('score', 10);
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Person');
|
||||
q.greaterThanOrEqualTo('score', 15);
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
q.greaterThanOrEqualTo('score', 12);
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
q.greaterThanOrEqualTo('score', 10);
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Person');
|
||||
q.notEqualTo('score', 12);
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
q.notEqualTo('score', 40);
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
});
|
||||
|
||||
it('matches an $or query', function() {
|
||||
var player = {
|
||||
id: new Id('Player', 'P1'),
|
||||
name: 'Player 1',
|
||||
score: 12
|
||||
};
|
||||
var q = new Parse.Query('Player');
|
||||
q.equalTo('name', 'Player 1');
|
||||
var q2 = new Parse.Query('Player');
|
||||
q2.equalTo('name', 'Player 2');
|
||||
var orQuery = Parse.Query.or(q, q2);
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
expect(matchesQuery(player, q2)).toBe(false);
|
||||
expect(matchesQuery(player, orQuery)).toBe(true);
|
||||
});
|
||||
|
||||
it('matches $regex queries', function() {
|
||||
var player = {
|
||||
id: new Id('Player', 'P1'),
|
||||
name: 'Player 1',
|
||||
score: 12
|
||||
};
|
||||
|
||||
var q = new Parse.Query('Player');
|
||||
q.startsWith('name', 'Play');
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
q.startsWith('name', 'Ploy');
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
|
||||
q = new Parse.Query('Player');
|
||||
q.endsWith('name', ' 1');
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
q.endsWith('name', ' 2');
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
|
||||
// Check that special characters are escaped
|
||||
player.name = 'Android-7';
|
||||
q = new Parse.Query('Player');
|
||||
q.contains('name', 'd-7');
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Player');
|
||||
q.matches('name', /A.d/);
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
|
||||
q.matches('name', /A[^n]d/);
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
|
||||
// Check that the string \\E is returned to normal
|
||||
player.name = 'Slash \\E';
|
||||
q = new Parse.Query('Player');
|
||||
q.endsWith('name', 'h \\E');
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
|
||||
q.endsWith('name', 'h \\Ee');
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
|
||||
player.name = 'Slash \\Q and more';
|
||||
q = new Parse.Query('Player');
|
||||
q.contains('name', 'h \\Q and');
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
q.contains('name', 'h \\Q or');
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
});
|
||||
|
||||
it('matches $nearSphere queries', function() {
|
||||
var q = new Parse.Query('Checkin');
|
||||
q.near('location', new Parse.GeoPoint(20, 20));
|
||||
// With no max distance, any GeoPoint is 'near'
|
||||
var pt = {
|
||||
id: new Id('Checkin', 'C1'),
|
||||
location: new Parse.GeoPoint(40, 40)
|
||||
};
|
||||
expect(matchesQuery(pt, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Checkin');
|
||||
pt.location = new Parse.GeoPoint(40, 40);
|
||||
q.withinRadians('location', new Parse.GeoPoint(30, 30), 0.3);
|
||||
expect(matchesQuery(pt, q)).toBe(true);
|
||||
|
||||
q.withinRadians('location', new Parse.GeoPoint(30, 30), 0.2);
|
||||
expect(matchesQuery(pt, q)).toBe(false);
|
||||
});
|
||||
|
||||
it('matches $within queries', function() {
|
||||
var caltrainStation = {
|
||||
id: new Id('Checkin', 'C1'),
|
||||
location: new Parse.GeoPoint(37.776346, -122.394218),
|
||||
name: 'Caltrain'
|
||||
};
|
||||
|
||||
var santaClara = {
|
||||
id: new Id('Checkin', 'C2'),
|
||||
location: new Parse.GeoPoint(37.325635, -121.945753),
|
||||
name: 'Santa Clara'
|
||||
};
|
||||
|
||||
var q = new Parse.Query('Checkin').withinGeoBox(
|
||||
'location',
|
||||
new Parse.GeoPoint(37.708813, -122.526398),
|
||||
new Parse.GeoPoint(37.822802, -122.373962)
|
||||
);
|
||||
|
||||
expect(matchesQuery(caltrainStation, q)).toBe(true);
|
||||
expect(matchesQuery(santaClara, q)).toBe(false);
|
||||
|
||||
// Invalid rectangles
|
||||
q = new Parse.Query('Checkin').withinGeoBox(
|
||||
'location',
|
||||
new Parse.GeoPoint(37.822802, -122.373962),
|
||||
new Parse.GeoPoint(37.708813, -122.526398)
|
||||
);
|
||||
|
||||
expect(matchesQuery(caltrainStation, q)).toBe(false);
|
||||
expect(matchesQuery(santaClara, q)).toBe(false);
|
||||
|
||||
q = new Parse.Query('Checkin').withinGeoBox(
|
||||
'location',
|
||||
new Parse.GeoPoint(37.708813, -122.373962),
|
||||
new Parse.GeoPoint(37.822802, -122.526398)
|
||||
);
|
||||
|
||||
expect(matchesQuery(caltrainStation, q)).toBe(false);
|
||||
expect(matchesQuery(santaClara, q)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
|
||||
|
||||
describe('RedisPubSub', function() {
|
||||
|
||||
beforeEach(function(done) {
|
||||
// Mock redis
|
||||
var createClient = jasmine.createSpy('createClient');
|
||||
jasmine.mockLibrary('redis', 'createClient', createClient);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can create publisher', function() {
|
||||
var publisher = RedisPubSub.createPublisher('redisAddress');
|
||||
|
||||
var redis = require('redis');
|
||||
expect(redis.createClient).toHaveBeenCalledWith('redisAddress', { no_ready_check: true });
|
||||
});
|
||||
|
||||
it('can create subscriber', function() {
|
||||
var subscriber = RedisPubSub.createSubscriber('redisAddress');
|
||||
|
||||
var redis = require('redis');
|
||||
expect(redis.createClient).toHaveBeenCalledWith('redisAddress', { no_ready_check: true });
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
jasmine.restoreLibrary('redis', 'createClient');
|
||||
});
|
||||
});
|
||||
+168
-4
@@ -1,4 +1,4 @@
|
||||
// These tests check the "create" functionality of the REST API.
|
||||
// These tests check the "create" / "update" functionality of the REST API.
|
||||
var auth = require('../src/Auth');
|
||||
var cache = require('../src/cache');
|
||||
var Config = require('../src/Config');
|
||||
@@ -8,7 +8,7 @@ var rest = require('../src/rest');
|
||||
var request = require('request');
|
||||
|
||||
var config = new Config('test');
|
||||
var database = DatabaseAdapter.getDatabaseConnection('test');
|
||||
var database = DatabaseAdapter.getDatabaseConnection('test', 'test_');
|
||||
|
||||
describe('rest create', () => {
|
||||
it('handles _id', (done) => {
|
||||
@@ -41,6 +41,52 @@ describe('rest create', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handles object and subdocument', (done) => {
|
||||
var obj = {
|
||||
subdoc: {foo: 'bar', wu: 'tan'},
|
||||
};
|
||||
rest.create(config, auth.nobody(config), 'MyClass', obj).then(() => {
|
||||
return database.mongoFind('MyClass', {}, {});
|
||||
}).then((results) => {
|
||||
expect(results.length).toEqual(1);
|
||||
var mob = results[0];
|
||||
expect(typeof mob.subdoc).toBe('object');
|
||||
expect(mob.subdoc.foo).toBe('bar');
|
||||
expect(mob.subdoc.wu).toBe('tan');
|
||||
expect(typeof mob._id).toEqual('string');
|
||||
|
||||
var obj = {
|
||||
'subdoc.wu': 'clan',
|
||||
};
|
||||
|
||||
rest.update(config, auth.nobody(config), 'MyClass', mob._id, obj).then(() => {
|
||||
return database.mongoFind('MyClass', {}, {});
|
||||
}).then((results) => {
|
||||
expect(results.length).toEqual(1);
|
||||
var mob = results[0];
|
||||
expect(typeof mob.subdoc).toBe('object');
|
||||
expect(mob.subdoc.foo).toBe('bar');
|
||||
expect(mob.subdoc.wu).toBe('clan');
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
it('handles create on non-existent class when disabled client class creation', (done) => {
|
||||
var customConfig = Object.assign({}, config, {allowClientClassCreation: false});
|
||||
rest.create(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {})
|
||||
.then(() => {
|
||||
fail('Should throw an error');
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
|
||||
expect(err.message).toEqual('This user is not allowed to access ' +
|
||||
'non-existent class: ClientClassCreation');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles user signup', (done) => {
|
||||
var user = {
|
||||
username: 'asdf',
|
||||
@@ -100,9 +146,49 @@ describe('rest create', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('handles anonymous user signup and upgrade to new user', (done) => {
|
||||
var data1 = {
|
||||
authData: {
|
||||
anonymous: {
|
||||
id: '00000000-0000-0000-0000-000000000001'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var updatedData = {
|
||||
authData: { anonymous: null },
|
||||
username: 'hello',
|
||||
password: 'world'
|
||||
}
|
||||
var username1;
|
||||
var objectId;
|
||||
rest.create(config, auth.nobody(config), '_User', data1)
|
||||
.then((r) => {
|
||||
expect(typeof r.response.objectId).toEqual('string');
|
||||
expect(typeof r.response.createdAt).toEqual('string');
|
||||
expect(typeof r.response.sessionToken).toEqual('string');
|
||||
objectId = r.response.objectId;
|
||||
return auth.getAuthForSessionToken({config, sessionToken: r.response.sessionToken })
|
||||
}).then((sessionAuth) => {
|
||||
return rest.update(config, sessionAuth, '_User', objectId, updatedData);
|
||||
}).then((r) => {
|
||||
return Parse.User.logOut().then(() => {
|
||||
return Parse.User.logIn('hello', 'world');
|
||||
})
|
||||
}).then((r) => {
|
||||
expect(r.id).toEqual(objectId);
|
||||
expect(r.get('username')).toEqual('hello');
|
||||
done();
|
||||
}).catch((err) => {
|
||||
fail('should not fail')
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('handles no anonymous users config', (done) => {
|
||||
var NoAnnonConfig = Object.assign({}, config, {enableAnonymousUsers: false});
|
||||
var NoAnnonConfig = Object.assign({}, config);
|
||||
NoAnnonConfig.authDataManager.setEnableAnonymousUsers(false);
|
||||
var data1 = {
|
||||
authData: {
|
||||
anonymous: {
|
||||
@@ -116,6 +202,7 @@ describe('rest create', () => {
|
||||
}, (err) => {
|
||||
expect(err.code).toEqual(Parse.Error.UNSUPPORTED_SERVICE);
|
||||
expect(err.message).toEqual('This authentication method is unsupported.');
|
||||
NoAnnonConfig.authDataManager.setEnableAnonymousUsers(true);
|
||||
done();
|
||||
})
|
||||
});
|
||||
@@ -129,17 +216,26 @@ describe('rest create', () => {
|
||||
}
|
||||
}
|
||||
};
|
||||
var newUserSignedUpByFacebookObjectId;
|
||||
rest.create(config, auth.nobody(config), '_User', data)
|
||||
.then((r) => {
|
||||
expect(typeof r.response.objectId).toEqual('string');
|
||||
expect(typeof r.response.createdAt).toEqual('string');
|
||||
expect(typeof r.response.sessionToken).toEqual('string');
|
||||
newUserSignedUpByFacebookObjectId = r.response.objectId;
|
||||
return rest.create(config, auth.nobody(config), '_User', data);
|
||||
}).then((r) => {
|
||||
expect(typeof r.response.objectId).toEqual('string');
|
||||
expect(typeof r.response.createdAt).toEqual('string');
|
||||
expect(typeof r.response.username).toEqual('string');
|
||||
expect(typeof r.response.updatedAt).toEqual('string');
|
||||
expect(r.response.objectId).toEqual(newUserSignedUpByFacebookObjectId);
|
||||
return rest.find(config, auth.master(config),
|
||||
'_Session', {sessionToken: r.response.sessionToken});
|
||||
}).then((response) => {
|
||||
expect(response.results.length).toEqual(1);
|
||||
var output = response.results[0];
|
||||
expect(output.user.objectId).toEqual(newUserSignedUpByFacebookObjectId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -188,4 +284,72 @@ describe('rest create', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("test default session length", (done) => {
|
||||
var user = {
|
||||
username: 'asdf',
|
||||
password: 'zxcv',
|
||||
foo: 'bar',
|
||||
};
|
||||
var now = new Date();
|
||||
|
||||
rest.create(config, auth.nobody(config), '_User', user)
|
||||
.then((r) => {
|
||||
expect(Object.keys(r.response).length).toEqual(3);
|
||||
expect(typeof r.response.objectId).toEqual('string');
|
||||
expect(typeof r.response.createdAt).toEqual('string');
|
||||
expect(typeof r.response.sessionToken).toEqual('string');
|
||||
return rest.find(config, auth.master(config),
|
||||
'_Session', {sessionToken: r.response.sessionToken});
|
||||
})
|
||||
.then((r) => {
|
||||
expect(r.results.length).toEqual(1);
|
||||
|
||||
var session = r.results[0];
|
||||
var actual = new Date(session.expiresAt.iso);
|
||||
var expected = new Date(now.getTime() + (1000 * 3600 * 24 * 365));
|
||||
|
||||
expect(actual.getFullYear()).toEqual(expected.getFullYear());
|
||||
expect(actual.getMonth()).toEqual(expected.getMonth());
|
||||
expect(actual.getDate()).toEqual(expected.getDate());
|
||||
expect(actual.getMinutes()).toEqual(expected.getMinutes());
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("test specified session length", (done) => {
|
||||
var user = {
|
||||
username: 'asdf',
|
||||
password: 'zxcv',
|
||||
foo: 'bar',
|
||||
};
|
||||
var sessionLength = 3600, // 1 Hour ahead
|
||||
now = new Date(); // For reference later
|
||||
config.sessionLength = sessionLength;
|
||||
|
||||
rest.create(config, auth.nobody(config), '_User', user)
|
||||
.then((r) => {
|
||||
expect(Object.keys(r.response).length).toEqual(3);
|
||||
expect(typeof r.response.objectId).toEqual('string');
|
||||
expect(typeof r.response.createdAt).toEqual('string');
|
||||
expect(typeof r.response.sessionToken).toEqual('string');
|
||||
return rest.find(config, auth.master(config),
|
||||
'_Session', {sessionToken: r.response.sessionToken});
|
||||
})
|
||||
.then((r) => {
|
||||
expect(r.results.length).toEqual(1);
|
||||
|
||||
var session = r.results[0];
|
||||
var actual = new Date(session.expiresAt.iso);
|
||||
var expected = new Date(now.getTime() + (sessionLength*1000));
|
||||
|
||||
expect(actual.getFullYear()).toEqual(expected.getFullYear());
|
||||
expect(actual.getMonth()).toEqual(expected.getMonth());
|
||||
expect(actual.getDate()).toEqual(expected.getDate());
|
||||
expect(actual.getHours()).toEqual(expected.getHours());
|
||||
expect(actual.getMinutes()).toEqual(expected.getMinutes());
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,9 @@ var cache = require('../src/cache');
|
||||
var Config = require('../src/Config');
|
||||
var rest = require('../src/rest');
|
||||
|
||||
var querystring = require('querystring');
|
||||
var request = require('request');
|
||||
|
||||
var config = new Config('test');
|
||||
var nobody = auth.nobody(config);
|
||||
|
||||
@@ -92,4 +95,63 @@ describe('rest query', () => {
|
||||
}).catch((error) => { console.log(error); });
|
||||
});
|
||||
|
||||
it('query non-existent class when disabled client class creation', (done) => {
|
||||
var customConfig = Object.assign({}, config, {allowClientClassCreation: false});
|
||||
rest.find(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {})
|
||||
.then(() => {
|
||||
fail('Should throw an error');
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
|
||||
expect(err.message).toEqual('This user is not allowed to access ' +
|
||||
'non-existent class: ClientClassCreation');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('query with wrongly encoded parameter', (done) => {
|
||||
rest.create(config, nobody, 'TestParameterEncode', {foo: 'bar'}
|
||||
).then(() => {
|
||||
return rest.create(config, nobody,
|
||||
'TestParameterEncode', {foo: 'baz'});
|
||||
}).then(() => {
|
||||
var headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
};
|
||||
request.get({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/classes/TestParameterEncode?'
|
||||
+ querystring.stringify({
|
||||
where: '{"foo":{"$ne": "baz"}}',
|
||||
limit: 1
|
||||
}).replace('=', '%3D'),
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(b.code).toEqual(Parse.Error.INVALID_QUERY);
|
||||
expect(b.error).toEqual('Improper encode of parameter');
|
||||
done();
|
||||
});
|
||||
}).then(() => {
|
||||
var headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
};
|
||||
request.get({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/classes/TestParameterEncode?'
|
||||
+ querystring.stringify({
|
||||
limit: 1
|
||||
}).replace('=', '%3D'),
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(b.code).toEqual(Parse.Error.INVALID_QUERY);
|
||||
expect(b.error).toEqual('Improper encode of parameter');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
+171
-40
@@ -1,3 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
var Config = require('../src/Config');
|
||||
var Schema = require('../src/Schema');
|
||||
var dd = require('deep-diff');
|
||||
@@ -14,9 +16,6 @@ var hasAllPODobject = () => {
|
||||
obj.set('aArray', ['contents', true, 5]);
|
||||
obj.set('aGeoPoint', new Parse.GeoPoint({latitude: 0, longitude: 0}));
|
||||
obj.set('aFile', new Parse.File('f.txt', { base64: 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=' }));
|
||||
var objACL = new Parse.ACL();
|
||||
objACL.setPublicWriteAccess(false);
|
||||
obj.setACL(objACL);
|
||||
return obj;
|
||||
};
|
||||
|
||||
@@ -32,6 +31,17 @@ describe('Schema', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('can validate one object with dot notation', (done) => {
|
||||
config.database.loadSchema().then((schema) => {
|
||||
return schema.validateObject('TestObjectWithSubDoc', {x: false, y: 'YY', z: 1, 'aObject.k1': 'newValue'});
|
||||
}).then((schema) => {
|
||||
done();
|
||||
}, (error) => {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can validate two objects in a row', (done) => {
|
||||
config.database.loadSchema().then((schema) => {
|
||||
return schema.validateObject('Foo', {x: true, y: 'yyy', z: 0});
|
||||
@@ -170,20 +180,18 @@ describe('Schema', () => {
|
||||
|
||||
it('will fail to create a class if that class was already created by an object', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => {
|
||||
schema.validateObject('NewClass', {foo: 7})
|
||||
.then(() => {
|
||||
schema.reload()
|
||||
.then(schema => schema.addClassIfNotExists('NewClass', {
|
||||
foo: {type: 'String'}
|
||||
}))
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME)
|
||||
expect(error.error).toEqual('class NewClass already exists');
|
||||
done();
|
||||
});
|
||||
.then(schema => {
|
||||
schema.validateObject('NewClass', { foo: 7 })
|
||||
.then(() => schema.reloadData())
|
||||
.then(() => schema.addClassIfNotExists('NewClass', {
|
||||
foo: { type: 'String' }
|
||||
}))
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
||||
expect(error.message).toEqual('Class NewClass already exists.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('will resolve class creation races appropriately', done => {
|
||||
@@ -206,7 +214,7 @@ describe('Schema', () => {
|
||||
Promise.all([p1,p2])
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
||||
expect(error.error).toEqual('class NewClass already exists');
|
||||
expect(error.message).toEqual('Class NewClass already exists.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -412,6 +420,43 @@ describe('Schema', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('creates non-custom classes which include relation field', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('_Role', {}))
|
||||
.then(mongoObj => {
|
||||
expect(mongoObj).toEqual({
|
||||
_id: '_Role',
|
||||
createdAt: 'string',
|
||||
updatedAt: 'string',
|
||||
objectId: 'string',
|
||||
name: 'string',
|
||||
users: 'relation<_User>',
|
||||
roles: 'relation<_Role>',
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('creates non-custom classes which include pointer field', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('_Session', {}))
|
||||
.then(mongoObj => {
|
||||
expect(mongoObj).toEqual({
|
||||
_id: '_Session',
|
||||
createdAt: 'string',
|
||||
updatedAt: 'string',
|
||||
objectId: 'string',
|
||||
restricted: 'boolean',
|
||||
user: '*_User',
|
||||
installationId: 'string',
|
||||
sessionToken: 'string',
|
||||
expiresAt: 'date',
|
||||
createdWith: 'object'
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses to create two geopoints', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('NewClass', {
|
||||
@@ -475,7 +520,7 @@ describe('Schema', () => {
|
||||
.then(schema => schema.deleteField('installationId', '_Installation'))
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(136);
|
||||
expect(error.error).toEqual('field installationId cannot be changed');
|
||||
expect(error.message).toEqual('field installationId cannot be changed');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -485,7 +530,7 @@ describe('Schema', () => {
|
||||
.then(schema => schema.deleteField('field', 'NoClass'))
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
||||
expect(error.error).toEqual('class NoClass does not exist');
|
||||
expect(error.message).toEqual('Class NoClass does not exist.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -496,7 +541,7 @@ describe('Schema', () => {
|
||||
.then(schema => schema.deleteField('missingField', 'HasAllPOD'))
|
||||
.fail(error => {
|
||||
expect(error.code).toEqual(255);
|
||||
expect(error.error).toEqual('field missingField does not exist, cannot delete');
|
||||
expect(error.message).toEqual('Field missingField does not exist, cannot delete.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -504,24 +549,64 @@ describe('Schema', () => {
|
||||
it('drops related collection when deleting relation field', done => {
|
||||
var obj1 = hasAllPODobject();
|
||||
obj1.save()
|
||||
.then(savedObj1 => {
|
||||
var obj2 = new Parse.Object('HasPointersAndRelations');
|
||||
obj2.set('aPointer', savedObj1);
|
||||
var relation = obj2.relation('aRelation');
|
||||
relation.add(obj1);
|
||||
return obj2.save();
|
||||
})
|
||||
.then(() => {
|
||||
config.database.db.collection('test__Join:aRelation:HasPointersAndRelations', { strict: true }, (err, coll) => {
|
||||
expect(err).toEqual(null);
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.deleteField('aRelation', 'HasPointersAndRelations', config.database.db, 'test_'))
|
||||
.then(() => config.database.db.collection('test__Join:aRelation:HasPointersAndRelations', { strict: true }, (err, coll) => {
|
||||
expect(err).not.toEqual(null);
|
||||
done();
|
||||
}))
|
||||
.then(savedObj1 => {
|
||||
var obj2 = new Parse.Object('HasPointersAndRelations');
|
||||
obj2.set('aPointer', savedObj1);
|
||||
var relation = obj2.relation('aRelation');
|
||||
relation.add(obj1);
|
||||
return obj2.save();
|
||||
})
|
||||
.then(() => config.database.collectionExists('_Join:aRelation:HasPointersAndRelations'))
|
||||
.then(exists => {
|
||||
if (!exists) {
|
||||
fail('Relation collection ' +
|
||||
'should exist after save.');
|
||||
}
|
||||
})
|
||||
.then(() => config.database.loadSchema())
|
||||
.then(schema => schema.deleteField('aRelation', 'HasPointersAndRelations', config.database))
|
||||
.then(() => config.database.collectionExists('_Join:aRelation:HasPointersAndRelations'))
|
||||
.then(exists => {
|
||||
if (exists) {
|
||||
fail('Relation collection should not exist after deleting relation field.');
|
||||
}
|
||||
done();
|
||||
}, error => {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('can delete relation field when related _Join collection not exist', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => {
|
||||
schema.addClassIfNotExists('NewClass', {
|
||||
relationField: {type: 'Relation', targetClass: '_User'}
|
||||
})
|
||||
.then(mongoObj => {
|
||||
expect(mongoObj).toEqual({
|
||||
_id: 'NewClass',
|
||||
objectId: 'string',
|
||||
updatedAt: 'string',
|
||||
createdAt: 'string',
|
||||
relationField: 'relation<_User>',
|
||||
});
|
||||
})
|
||||
.then(() => config.database.collectionExists('_Join:relationField:NewClass'))
|
||||
.then(exist => {
|
||||
expect(exist).toEqual(false);
|
||||
})
|
||||
.then(() => schema.deleteField('relationField', 'NewClass', config.database))
|
||||
.then(() => schema.reloadData())
|
||||
.then(() => {
|
||||
expect(schema['data']['NewClass']).toEqual({
|
||||
objectId: 'string',
|
||||
updatedAt: 'string',
|
||||
createdAt: 'string'
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can delete string fields and resave as number field', done => {
|
||||
@@ -530,7 +615,7 @@ describe('Schema', () => {
|
||||
var obj2 = hasAllPODobject();
|
||||
var p = Parse.Object.saveAll([obj1, obj2])
|
||||
.then(() => config.database.loadSchema())
|
||||
.then(schema => schema.deleteField('aString', 'HasAllPOD', config.database.db, 'test_'))
|
||||
.then(schema => schema.deleteField('aString', 'HasAllPOD', config.database))
|
||||
.then(() => new Parse.Query('HasAllPOD').get(obj1.id))
|
||||
.then(obj1Reloaded => {
|
||||
expect(obj1Reloaded.get('aString')).toEqual(undefined);
|
||||
@@ -545,7 +630,7 @@ describe('Schema', () => {
|
||||
done();
|
||||
Parse.Object.enableSingleInstance();
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('can delete pointer fields and resave as string', done => {
|
||||
@@ -560,7 +645,7 @@ describe('Schema', () => {
|
||||
expect(obj1.get('aPointer').id).toEqual(obj1.id);
|
||||
})
|
||||
.then(() => config.database.loadSchema())
|
||||
.then(schema => schema.deleteField('aPointer', 'NewClass', config.database.db, 'test_'))
|
||||
.then(schema => schema.deleteField('aPointer', 'NewClass', config.database))
|
||||
.then(() => new Parse.Query('NewClass').get(obj1.id))
|
||||
.then(obj1 => {
|
||||
expect(obj1.get('aPointer')).toEqual(undefined);
|
||||
@@ -601,4 +686,50 @@ describe('Schema', () => {
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
it('ignore default field when merge with system class', done => {
|
||||
expect(Schema.buildMergedSchemaObject({
|
||||
_id: '_User',
|
||||
username: 'string',
|
||||
password: 'string',
|
||||
authData: 'object',
|
||||
email: 'string',
|
||||
emailVerified: 'boolean'
|
||||
},{
|
||||
authData: {type: 'string'},
|
||||
customField: {type: 'string'},
|
||||
})).toEqual({
|
||||
customField: {type: 'string'}
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
it('handles legacy _client_permissions keys without crashing', done => {
|
||||
Schema.mongoSchemaToSchemaAPIResponse({
|
||||
"_id":"_Installation",
|
||||
"_client_permissions":{
|
||||
"get":true,
|
||||
"find":true,
|
||||
"update":true,
|
||||
"create":true,
|
||||
"delete":true,
|
||||
},
|
||||
"_metadata":{
|
||||
"class_permissions":{
|
||||
"get":{"*":true},
|
||||
"find":{"*":true},
|
||||
"update":{"*":true},
|
||||
"create":{"*":true},
|
||||
"delete":{"*":true},
|
||||
"addField":{"*":true},
|
||||
}
|
||||
},
|
||||
"installationId":"string",
|
||||
"deviceToken":"string",
|
||||
"deviceType":"string",
|
||||
"channels":"array",
|
||||
"user":"*_User",
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
var SessionTokenCache = require('../src/LiveQuery/SessionTokenCache').SessionTokenCache;
|
||||
|
||||
describe('SessionTokenCache', function() {
|
||||
|
||||
beforeEach(function(done) {
|
||||
var Parse = require('parse/node');
|
||||
// Mock parse
|
||||
var mockUser = {
|
||||
become: jasmine.createSpy('become').and.returnValue(Parse.Promise.as({
|
||||
id: 'userId'
|
||||
}))
|
||||
}
|
||||
jasmine.mockLibrary('parse/node', 'User', mockUser);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can get undefined userId', function(done) {
|
||||
var sessionTokenCache = new SessionTokenCache();
|
||||
|
||||
sessionTokenCache.getUserId(undefined).then((userIdFromCache) => {
|
||||
}, (error) => {
|
||||
expect(error).not.toBeNull();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get existing userId', function(done) {
|
||||
var sessionTokenCache = new SessionTokenCache();
|
||||
var sessionToken = 'sessionToken';
|
||||
var userId = 'userId'
|
||||
sessionTokenCache.cache.set(sessionToken, userId);
|
||||
|
||||
sessionTokenCache.getUserId(sessionToken).then((userIdFromCache) => {
|
||||
expect(userIdFromCache).toBe(userId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get new userId', function(done) {
|
||||
var sessionTokenCache = new SessionTokenCache();
|
||||
|
||||
sessionTokenCache.getUserId('sessionToken').then((userIdFromCache) => {
|
||||
expect(userIdFromCache).toBe('userId');
|
||||
expect(sessionTokenCache.cache.length).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
jasmine.restoreLibrary('parse/node', 'User');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
var Subscription = require('../src/LiveQuery/Subscription').Subscription;
|
||||
|
||||
describe('Subscription', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
var mockError = jasmine.createSpy('error');
|
||||
jasmine.mockLibrary('../src/LiveQuery/PLog', 'error', mockError);
|
||||
});
|
||||
|
||||
it('can be initialized', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
|
||||
expect(subscription.className).toBe('className');
|
||||
expect(subscription.query).toEqual({ key : 'value' });
|
||||
expect(subscription.hash).toBe('hash');
|
||||
expect(subscription.clientRequestIds.size).toBe(0);
|
||||
});
|
||||
|
||||
it('can check it has subscribing clients', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
|
||||
expect(subscription.hasSubscribingClient()).toBe(false);
|
||||
});
|
||||
|
||||
it('can check it does not have subscribing clients', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
subscription.addClientSubscription(1, 1);
|
||||
|
||||
expect(subscription.hasSubscribingClient()).toBe(true);
|
||||
});
|
||||
|
||||
it('can add one request for one client', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
subscription.addClientSubscription(1, 1);
|
||||
|
||||
expect(subscription.clientRequestIds.size).toBe(1);
|
||||
expect(subscription.clientRequestIds.get(1)).toEqual([1]);
|
||||
});
|
||||
|
||||
it('can add requests for one client', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
subscription.addClientSubscription(1, 1);
|
||||
subscription.addClientSubscription(1, 2);
|
||||
|
||||
expect(subscription.clientRequestIds.size).toBe(1);
|
||||
expect(subscription.clientRequestIds.get(1)).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it('can add requests for clients', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
subscription.addClientSubscription(1, 1);
|
||||
subscription.addClientSubscription(1, 2);
|
||||
subscription.addClientSubscription(2, 2);
|
||||
subscription.addClientSubscription(2, 3);
|
||||
|
||||
expect(subscription.clientRequestIds.size).toBe(2);
|
||||
expect(subscription.clientRequestIds.get(1)).toEqual([1, 2]);
|
||||
expect(subscription.clientRequestIds.get(2)).toEqual([2, 3]);
|
||||
});
|
||||
|
||||
it('can delete requests for nonexistent client', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
subscription.deleteClientSubscription(1, 1);
|
||||
|
||||
var PLog =require('../src/LiveQuery/PLog');
|
||||
expect(PLog.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can delete nonexistent request for one client', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
subscription.addClientSubscription(1, 1);
|
||||
subscription.deleteClientSubscription(1, 2);
|
||||
|
||||
var PLog =require('../src/LiveQuery/PLog');
|
||||
expect(PLog.error).toHaveBeenCalled();
|
||||
expect(subscription.clientRequestIds.size).toBe(1);
|
||||
expect(subscription.clientRequestIds.get(1)).toEqual([1]);
|
||||
});
|
||||
|
||||
it('can delete some requests for one client', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
subscription.addClientSubscription(1, 1);
|
||||
subscription.addClientSubscription(1, 2);
|
||||
subscription.deleteClientSubscription(1, 2);
|
||||
|
||||
var PLog =require('../src/LiveQuery/PLog');
|
||||
expect(PLog.error).not.toHaveBeenCalled();
|
||||
expect(subscription.clientRequestIds.size).toBe(1);
|
||||
expect(subscription.clientRequestIds.get(1)).toEqual([1]);
|
||||
});
|
||||
|
||||
it('can delete all requests for one client', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
subscription.addClientSubscription(1, 1);
|
||||
subscription.addClientSubscription(1, 2);
|
||||
subscription.deleteClientSubscription(1, 1);
|
||||
subscription.deleteClientSubscription(1, 2);
|
||||
|
||||
var PLog =require('../src/LiveQuery/PLog');
|
||||
expect(PLog.error).not.toHaveBeenCalled();
|
||||
expect(subscription.clientRequestIds.size).toBe(0);
|
||||
});
|
||||
|
||||
it('can delete requests for multiple clients', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
subscription.addClientSubscription(1, 1);
|
||||
subscription.addClientSubscription(1, 2);
|
||||
subscription.addClientSubscription(2, 1);
|
||||
subscription.addClientSubscription(2, 2);
|
||||
subscription.deleteClientSubscription(1, 2);
|
||||
subscription.deleteClientSubscription(2, 1);
|
||||
subscription.deleteClientSubscription(2, 2);
|
||||
|
||||
var PLog =require('../src/LiveQuery/PLog');
|
||||
expect(PLog.error).not.toHaveBeenCalled();
|
||||
expect(subscription.clientRequestIds.size).toBe(1);
|
||||
expect(subscription.clientRequestIds.get(1)).toEqual([1]);
|
||||
});
|
||||
|
||||
afterEach(function(){
|
||||
jasmine.restoreLibrary('../src/LiveQuery/PLog', 'error');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,679 @@
|
||||
"use strict";
|
||||
|
||||
var request = require('request');
|
||||
var Config = require("../src/Config");
|
||||
describe("Custom Pages Configuration", () => {
|
||||
it("should set the custom pages", (done) => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
customPages: {
|
||||
invalidLink: "myInvalidLink",
|
||||
verifyEmailSuccess: "myVerifyEmailSuccess",
|
||||
choosePassword: "myChoosePassword",
|
||||
passwordResetSuccess: "myPasswordResetSuccess"
|
||||
},
|
||||
publicServerURL: "https://my.public.server.com/1"
|
||||
});
|
||||
|
||||
var config = new Config("test");
|
||||
|
||||
expect(config.invalidLinkURL).toEqual("myInvalidLink");
|
||||
expect(config.verifyEmailSuccessURL).toEqual("myVerifyEmailSuccess");
|
||||
expect(config.choosePasswordURL).toEqual("myChoosePassword");
|
||||
expect(config.passwordResetSuccessURL).toEqual("myPasswordResetSuccess");
|
||||
expect(config.verifyEmailURL).toEqual("https://my.public.server.com/1/apps/test/verify_email");
|
||||
expect(config.requestResetPasswordURL).toEqual("https://my.public.server.com/1/apps/test/request_password_reset");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Email Verification", () => {
|
||||
it('sends verification email if email verification is enabled', done => {
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve()
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
spyOn(emailAdapter, 'sendVerificationEmail');
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.setEmail('cool_guy@parse.com');
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
|
||||
user.fetch()
|
||||
.then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(false);
|
||||
done();
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('does not send verification email when verification is enabled and email is not set', done => {
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve()
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
spyOn(emailAdapter, 'sendVerificationEmail');
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
|
||||
user.fetch()
|
||||
.then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('does send a validation email when updating the email', done => {
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve()
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
spyOn(emailAdapter, 'sendVerificationEmail');
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
|
||||
user.fetch()
|
||||
.then((user) => {
|
||||
user.set("email", "cool_guy@parse.com");
|
||||
return user.save();
|
||||
}).then((user) => {
|
||||
return user.fetch();
|
||||
}).then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(false);
|
||||
// Wait as on update email, we need to fetch the username
|
||||
setTimeout(function(){
|
||||
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
|
||||
done();
|
||||
}, 200);
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('does send a validation email with valid verification link when updating the email', done => {
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve()
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
spyOn(emailAdapter, 'sendVerificationEmail').and.callFake((options) => {
|
||||
expect(options.link).not.toBeNull();
|
||||
expect(options.link).not.toMatch(/token=undefined/);
|
||||
Promise.resolve();
|
||||
});
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
|
||||
user.fetch()
|
||||
.then((user) => {
|
||||
user.set("email", "cool_guy@parse.com");
|
||||
return user.save();
|
||||
}).then((user) => {
|
||||
return user.fetch();
|
||||
}).then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(false);
|
||||
// Wait as on update email, we need to fetch the username
|
||||
setTimeout(function(){
|
||||
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
|
||||
done();
|
||||
}, 200);
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('does send with a simple adapter', done => {
|
||||
var calls = 0;
|
||||
var emailAdapter = {
|
||||
sendMail: function(options){
|
||||
expect(options.to).toBe('cool_guy@parse.com');
|
||||
if (calls == 0) {
|
||||
expect(options.subject).toEqual('Please verify your e-mail for My Cool App');
|
||||
expect(options.text.match(/verify_email/)).not.toBe(null);
|
||||
} else if (calls == 1) {
|
||||
expect(options.subject).toEqual('Password Reset for My Cool App');
|
||||
expect(options.text.match(/request_password_reset/)).not.toBe(null);
|
||||
}
|
||||
calls++;
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'My Cool App',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.set("email", "cool_guy@parse.com");
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
expect(calls).toBe(1);
|
||||
user.fetch()
|
||||
.then((user) => {
|
||||
return user.save();
|
||||
}).then((user) => {
|
||||
return Parse.User.requestPasswordReset("cool_guy@parse.com");
|
||||
}).then(() => {
|
||||
expect(calls).toBe(2);
|
||||
done();
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('does not send verification email if email verification is disabled', done => {
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve()
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: false,
|
||||
emailAdapter: emailAdapter,
|
||||
});
|
||||
spyOn(emailAdapter, 'sendVerificationEmail');
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
user.fetch()
|
||||
.then(() => {
|
||||
expect(emailAdapter.sendVerificationEmail.calls.count()).toEqual(0);
|
||||
expect(user.get('emailVerified')).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('receives the app name and user in the adapter', done => {
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
expect(options.appName).toEqual('emailing app');
|
||||
expect(options.user.get('email')).toEqual('user@parse.com');
|
||||
done();
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.set('email', 'user@parse.com');
|
||||
user.signUp(null, {
|
||||
success: () => {},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
it('when you click the link in the email it sets emailVerified to true and redirects you', done => {
|
||||
var user = new Parse.User();
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
request.get(options.link, {
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user');
|
||||
user.fetch()
|
||||
.then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(true);
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("this should not fail");
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("user");
|
||||
user.set('email', 'user@parse.com');
|
||||
user.signUp();
|
||||
});
|
||||
|
||||
it('redirects you to invalid link if you try to verify email incorrecly', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
},
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
request.get('http://localhost:8378/1/apps/test/verify_email', {
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
|
||||
done()
|
||||
});
|
||||
});
|
||||
|
||||
it('redirects you to invalid link if you try to validate a nonexistant users email', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
},
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
request.get('http://localhost:8378/1/apps/test/verify_email?token=asdfasdf&username=sadfasga', {
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not update email verified if you use an invalid token', done => {
|
||||
var user = new Parse.User();
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
request.get('http://localhost:8378/1/apps/test/verify_email?token=invalid&username=zxcv', {
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
|
||||
user.fetch()
|
||||
.then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.set('email', 'user@parse.com');
|
||||
user.signUp(null, {
|
||||
success: () => {},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Password Reset", () => {
|
||||
|
||||
it('should send a password reset link', done => {
|
||||
var user = new Parse.User();
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: options => {
|
||||
request.get(options.link, {
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
fail("Failed to get the reset link");
|
||||
return;
|
||||
}
|
||||
expect(response.statusCode).toEqual(302);
|
||||
var re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=zxcv%2Bzxcv/;
|
||||
expect(response.body.match(re)).not.toBe(null);
|
||||
done();
|
||||
});
|
||||
},
|
||||
sendMail: () => {}
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv+zxcv");
|
||||
user.set('email', 'user@parse.com');
|
||||
user.signUp().then(() => {
|
||||
Parse.User.requestPasswordReset('user@parse.com', {
|
||||
error: (err) => {
|
||||
console.error(err);
|
||||
fail("Should not fail");
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('redirects you to invalid link if you try to request password for a nonexistant users email', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
},
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
request.get('http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf&username=sadfasga', {
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should programatically reset password', done => {
|
||||
var user = new Parse.User();
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: options => {
|
||||
request.get(options.link, {
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
fail("Failed to get the reset link");
|
||||
return;
|
||||
}
|
||||
expect(response.statusCode).toEqual(302);
|
||||
var re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv/;
|
||||
var match = response.body.match(re);
|
||||
if (!match) {
|
||||
fail("should have a token");
|
||||
done();
|
||||
return;
|
||||
}
|
||||
var token = match[1];
|
||||
|
||||
request.post({
|
||||
url: "http://localhost:8378/1/apps/test/request_password_reset" ,
|
||||
body: `new_password=hello&token=${token}&username=zxcv`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
fail("Failed to POST request password reset");
|
||||
return;
|
||||
}
|
||||
expect(response.statusCode).toEqual(302);
|
||||
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html');
|
||||
|
||||
Parse.User.logIn("zxcv", "hello").then(function(user){
|
||||
let config = new Config('test');
|
||||
config.database.adaptiveCollection('_User')
|
||||
.then(coll => coll.find({ 'username': 'zxcv' }, { limit: 1 }))
|
||||
.then((results) => {
|
||||
// _perishable_token should be unset after reset password
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0]['_perishable_token']).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("should login with new password");
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
},
|
||||
sendMail: () => {}
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.set('email', 'user@parse.com');
|
||||
user.signUp().then(() => {
|
||||
Parse.User.requestPasswordReset('user@parse.com', {
|
||||
error: (err) => {
|
||||
console.error(err);
|
||||
fail("Should not fail");
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
@@ -1,5 +1,3 @@
|
||||
var Parse = require('parse/node').Parse;
|
||||
|
||||
Parse.Cloud.define('hello', function(req, res) {
|
||||
res.success('Hello world!');
|
||||
});
|
||||
@@ -102,3 +100,18 @@ Parse.Cloud.define('requiredParameterCheck', function(req, res) {
|
||||
}, function(params) {
|
||||
return params.name;
|
||||
});
|
||||
|
||||
Parse.Cloud.define('echoKeys', function(req, res){
|
||||
return res.success({
|
||||
applicationId: Parse.applicationId,
|
||||
masterKey: Parse.masterKey,
|
||||
javascriptKey: Parse.javascriptKey
|
||||
})
|
||||
});
|
||||
|
||||
Parse.Cloud.define('createBeforeSaveChangedObject', function(req, res){
|
||||
var obj = new Parse.Object('BeforeSaveChanged');
|
||||
obj.save().then(() => {
|
||||
res.success(obj);
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"arg1": "my_app",
|
||||
"arg2": "8888",
|
||||
"arg3": "hello",
|
||||
"arg4": "/1"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"arg1": "my_app",
|
||||
"arg2": 8888,
|
||||
"arg3": "hello",
|
||||
"arg4": "/1"
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"arg1": "my_app",
|
||||
"arg2": "hello",
|
||||
"arg3": "hello",
|
||||
"arg4": "/1"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"arg1": "my_app",
|
||||
"arg2": "99999",
|
||||
"arg3": "hello",
|
||||
"arg4": "/1"
|
||||
},
|
||||
{
|
||||
"arg1": "my_app2",
|
||||
"arg2": "9999",
|
||||
"arg3": "hello",
|
||||
"arg4": "/1"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"arg1": "my_app",
|
||||
"arg2": "8888",
|
||||
"arg3": "hello",
|
||||
"myArg": "/1"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
const request = require("request");
|
||||
|
||||
describe('features', () => {
|
||||
it('requires the master key to get features', done => {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/serverInfo',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(403);
|
||||
expect(body.error).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
+65
-8
@@ -2,19 +2,22 @@
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000;
|
||||
|
||||
var cache = require('../src/cache');
|
||||
var cache = require('../src/cache').default;
|
||||
var DatabaseAdapter = require('../src/DatabaseAdapter');
|
||||
var express = require('express');
|
||||
var facebook = require('../src/oauth/facebook');
|
||||
var facebook = require('../src/authDataManager/facebook');
|
||||
var ParseServer = require('../src/index').ParseServer;
|
||||
var path = require('path');
|
||||
|
||||
var databaseURI = process.env.DATABASE_URI;
|
||||
var cloudMain = process.env.CLOUD_CODE_MAIN || './cloud/main.js';
|
||||
var cloudMain = process.env.CLOUD_CODE_MAIN || './spec/cloud/main.js';
|
||||
var port = 8378;
|
||||
|
||||
// Set up an API server for testing
|
||||
var api = new ParseServer({
|
||||
// Default server configuration for tests.
|
||||
var defaultConfiguration = {
|
||||
databaseURI: databaseURI,
|
||||
cloud: cloudMain,
|
||||
serverURL: 'http://localhost:' + port + '/1',
|
||||
appId: 'test',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
@@ -23,19 +26,50 @@ var api = new ParseServer({
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
push: {
|
||||
'ios': {
|
||||
cert: 'prodCert.pem',
|
||||
key: 'prodKey.pem',
|
||||
production: true,
|
||||
bundleId: 'bundleId'
|
||||
}
|
||||
},
|
||||
oauth: { // Override the facebook provider
|
||||
facebook: mockFacebook(),
|
||||
myoauth: {
|
||||
module: "../spec/myoauth" // relative path as it's run from src
|
||||
module: path.resolve(__dirname, "myoauth") // relative path as it's run from src
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Set up a default API server for testing with default configuration.
|
||||
var api = new ParseServer(defaultConfiguration);
|
||||
var app = express();
|
||||
app.use('/1', api);
|
||||
var port = 8378;
|
||||
var server = app.listen(port);
|
||||
|
||||
// Prevent reinitializing the server from clobbering Cloud Code
|
||||
delete defaultConfiguration.cloud;
|
||||
|
||||
var currentConfiguration;
|
||||
// Allows testing specific configurations of Parse Server
|
||||
var setServerConfiguration = configuration => {
|
||||
// the configuration hasn't changed
|
||||
if (configuration === currentConfiguration) {
|
||||
return;
|
||||
}
|
||||
DatabaseAdapter.clearDatabaseSettings();
|
||||
currentConfiguration = configuration;
|
||||
server.close();
|
||||
cache.clearCache();
|
||||
app = express();
|
||||
api = new ParseServer(configuration);
|
||||
app.use('/1', api);
|
||||
server = app.listen(port);
|
||||
};
|
||||
|
||||
var restoreServerConfiguration = () => setServerConfiguration(defaultConfiguration);
|
||||
|
||||
// Set up a Parse client to talk to our test API server
|
||||
var Parse = require('parse/node');
|
||||
Parse.serverURL = 'http://localhost:' + port + '/1';
|
||||
@@ -45,7 +79,9 @@ Parse.serverURL = 'http://localhost:' + port + '/1';
|
||||
Parse.Promise.disableAPlusCompliant();
|
||||
|
||||
beforeEach(function(done) {
|
||||
restoreServerConfiguration();
|
||||
Parse.initialize('test', 'test', 'test');
|
||||
Parse.serverURL = 'http://localhost:' + port + '/1';
|
||||
Parse.User.enableUnsafeCurrentUser();
|
||||
done();
|
||||
});
|
||||
@@ -221,3 +257,24 @@ global.expectError = expectError;
|
||||
global.arrayContains = arrayContains;
|
||||
global.jequal = jequal;
|
||||
global.range = range;
|
||||
global.setServerConfiguration = setServerConfiguration;
|
||||
global.defaultConfiguration = defaultConfiguration;
|
||||
|
||||
// LiveQuery test setting
|
||||
require('../src/LiveQuery/PLog').logLevel = 'NONE';
|
||||
var libraryCache = {};
|
||||
jasmine.mockLibrary = function(library, name, mock) {
|
||||
var original = require(library)[name];
|
||||
if (!libraryCache[library]) {
|
||||
libraryCache[library] = {};
|
||||
}
|
||||
require(library)[name] = mock;
|
||||
libraryCache[library][name] = original;
|
||||
}
|
||||
|
||||
jasmine.restoreLibrary = function(library, name) {
|
||||
if (!libraryCache[library] || !libraryCache[library][name]) {
|
||||
throw 'Can not find library ' + library + ' ' + name;
|
||||
}
|
||||
require(library)[name] = libraryCache[library][name];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
var request = require('request');
|
||||
var parseServerPackage = require('../package.json');
|
||||
var MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions');
|
||||
var ParseServer = require("../src/index");
|
||||
var Config = require('../src/Config');
|
||||
var express = require('express');
|
||||
|
||||
describe('server', () => {
|
||||
it('requires a master key and app id', done => {
|
||||
expect(setServerConfiguration.bind(undefined, { })).toThrow('You must provide an appId!');
|
||||
expect(setServerConfiguration.bind(undefined, { appId: 'myId' })).toThrow('You must provide a masterKey!');
|
||||
expect(setServerConfiguration.bind(undefined, { appId: 'myId', masterKey: 'mk' })).toThrow('You must provide a serverURL!');
|
||||
done();
|
||||
});
|
||||
|
||||
it('fails if database is unreachable', done => {
|
||||
setServerConfiguration({
|
||||
databaseURI: 'mongodb://fake:fake@ds043605.mongolab.com:43605/drew3',
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
});
|
||||
//Need to use rest api because saving via JS SDK results in fail() not getting called
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/classes/NewClass',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
body: {},
|
||||
json: true,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(500);
|
||||
expect(body.code).toEqual(1);
|
||||
expect(body.message).toEqual('Internal server error.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can load email adapter via object', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
it('can load email adapter via class', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: {
|
||||
class: MockEmailAdapterWithOptions,
|
||||
options: {
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}
|
||||
},
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
it('can load email adapter via module name', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: {
|
||||
module: 'parse-server-simple-mailgun-adapter',
|
||||
options: {
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}
|
||||
},
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
it('can load email adapter via only module name', done => {
|
||||
expect(() => setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: 'parse-server-simple-mailgun-adapter',
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
})).toThrow('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.');
|
||||
done();
|
||||
});
|
||||
|
||||
it('throws if you initialize email adapter incorrecly', done => {
|
||||
expect(() => setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: {
|
||||
module: 'parse-server-simple-mailgun-adapter',
|
||||
options: {
|
||||
domain: 'd',
|
||||
}
|
||||
},
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
})).toThrow('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.');
|
||||
done();
|
||||
});
|
||||
|
||||
it('can report the server version', done => {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/serverInfo',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
json: true,
|
||||
}, (error, response, body) => {
|
||||
expect(body.parseServerVersion).toEqual(parseServerPackage.version);
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('can load absolute cloud code file', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
masterKey: 'test',
|
||||
cloud: __dirname + '/cloud/main.js'
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
it('can load relative cloud code file', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
masterKey: 'test',
|
||||
cloud: './spec/cloud/main.js'
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
it('can create a parse-server', done => {
|
||||
var parseServer = new ParseServer.default({
|
||||
appId: "aTestApp",
|
||||
masterKey: "aTestMasterKey",
|
||||
serverURL: "http://localhost:12666/parse",
|
||||
databaseURI: 'mongodb://localhost:27017/aTestApp'
|
||||
});
|
||||
|
||||
expect(Parse.applicationId).toEqual("aTestApp");
|
||||
var app = express();
|
||||
app.use('/parse', parseServer.app);
|
||||
|
||||
var server = app.listen(12666);
|
||||
var obj = new Parse.Object("AnObject");
|
||||
var objId;
|
||||
obj.save().then((obj) => {
|
||||
objId = obj.id;
|
||||
var q = new Parse.Query("AnObject");
|
||||
return q.first();
|
||||
}).then((obj) => {
|
||||
expect(obj.id).toEqual(objId);
|
||||
server.close();
|
||||
done();
|
||||
}).fail((err) => {
|
||||
server.close();
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('can create a parse-server', done => {
|
||||
var parseServer = ParseServer.ParseServer({
|
||||
appId: "anOtherTestApp",
|
||||
masterKey: "anOtherTestMasterKey",
|
||||
serverURL: "http://localhost:12667/parse",
|
||||
databaseURI: 'mongodb://localhost:27017/anotherTstApp'
|
||||
});
|
||||
|
||||
expect(Parse.applicationId).toEqual("anOtherTestApp");
|
||||
var app = express();
|
||||
app.use('/parse', parseServer);
|
||||
|
||||
var server = app.listen(12667);
|
||||
var obj = new Parse.Object("AnObject");
|
||||
var objId;
|
||||
obj.save().then((obj) => {
|
||||
objId = obj.id;
|
||||
var q = new Parse.Query("AnObject");
|
||||
return q.first();
|
||||
}).then((obj) => {
|
||||
expect(obj.id).toEqual(objId);
|
||||
server.close();
|
||||
done();
|
||||
}).fail((err) => {
|
||||
server.close();
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('has createLiveQueryServer', done => {
|
||||
// original implementation through the factory
|
||||
expect(typeof ParseServer.ParseServer.createLiveQueryServer).toEqual('function');
|
||||
// For import calls
|
||||
expect(typeof ParseServer.default.createLiveQueryServer).toEqual('function');
|
||||
done();
|
||||
});
|
||||
|
||||
it('exposes all the "core" adapters', done => {
|
||||
expect(ParseServer.S3Adapter).toThrow();
|
||||
expect(ParseServer.GCSAdapter).toThrow('GCSAdapter requires an projectId');
|
||||
expect(ParseServer.FileSystemAdapter).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('properly gives publicServerURL when set', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
masterKey: 'test',
|
||||
publicServerURL: 'https://myserver.com/1'
|
||||
});
|
||||
var config = new Config('test', 'http://localhost:8378/1');
|
||||
expect(config.mount).toEqual('https://myserver.com/1');
|
||||
done();
|
||||
});
|
||||
|
||||
it('properly removes trailing slash in mount', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
masterKey: 'test'
|
||||
});
|
||||
var config = new Config('test', 'http://localhost:8378/1/');
|
||||
expect(config.mount).toEqual('http://localhost:8378/1');
|
||||
done();
|
||||
});
|
||||
|
||||
it('should throw when getting invalid mount', done => {
|
||||
expect(() => setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
masterKey: 'test',
|
||||
publicServerURL: 'blabla:/some'
|
||||
}) ).toThrow("publicServerURL should be a valid HTTPS URL starting with https://");
|
||||
done();
|
||||
});
|
||||
|
||||
it('fails if the session length is not a number', (done) => {
|
||||
expect(() => setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
masterKey: 'test',
|
||||
sessionLength: 'test'
|
||||
})).toThrow('Session length must be a valid number.');
|
||||
done();
|
||||
});
|
||||
|
||||
it('fails if the session length is less than or equal to 0', (done) => {
|
||||
expect(() => setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
masterKey: 'test',
|
||||
sessionLength: '-33'
|
||||
})).toThrow('Session length must be a value greater than 0.');
|
||||
|
||||
expect(() => setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
masterKey: 'test',
|
||||
sessionLength: '0'
|
||||
})).toThrow('Session length must be a value greater than 0.');
|
||||
done();
|
||||
})
|
||||
});
|
||||
+916
-22
File diff suppressed because it is too large
Load Diff
-208
@@ -1,208 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const Parse = require('parse/node').Parse;
|
||||
// TODO: apn does not support the new HTTP/2 protocal. It is fine to use it in V1,
|
||||
// but probably we will replace it in the future.
|
||||
const apn = require('apn');
|
||||
|
||||
/**
|
||||
* Create a new connection to the APN service.
|
||||
* @constructor
|
||||
* @param {Object|Array} args An argument or a list of arguments to config APNS connection
|
||||
* @param {String} args.cert The filename of the connection certificate to load from disk
|
||||
* @param {String} args.key The filename of the connection key to load from disk
|
||||
* @param {String} args.pfx The filename for private key, certificate and CA certs in PFX or PKCS12 format, it will overwrite cert and key
|
||||
* @param {String} args.passphrase The passphrase for the connection key, if required
|
||||
* @param {String} args.bundleId The bundleId for cert
|
||||
* @param {Boolean} args.production Specifies which environment to connect to: Production (if true) or Sandbox
|
||||
*/
|
||||
function APNS(args) {
|
||||
// Since for ios, there maybe multiple cert/key pairs,
|
||||
// typePushConfig can be an array.
|
||||
let apnsArgsList = [];
|
||||
if (Array.isArray(args)) {
|
||||
apnsArgsList = apnsArgsList.concat(args);
|
||||
} else if (typeof args === 'object') {
|
||||
apnsArgsList.push(args);
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
'APNS Configuration is invalid');
|
||||
}
|
||||
|
||||
this.conns = [];
|
||||
for (let apnsArgs of apnsArgsList) {
|
||||
let conn = new apn.Connection(apnsArgs);
|
||||
if (!apnsArgs.bundleId) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
'BundleId is mssing for %j', apnsArgs);
|
||||
}
|
||||
conn.bundleId = apnsArgs.bundleId;
|
||||
// Set the priority of the conns, prod cert has higher priority
|
||||
if (apnsArgs.production) {
|
||||
conn.priority = 0;
|
||||
} else {
|
||||
conn.priority = 1;
|
||||
}
|
||||
|
||||
// Set apns client callbacks
|
||||
conn.on('connected', () => {
|
||||
console.log('APNS Connection %d Connected', conn.index);
|
||||
});
|
||||
|
||||
conn.on('transmissionError', (errCode, notification, apnDevice) => {
|
||||
handleTransmissionError(this.conns, errCode, notification, apnDevice);
|
||||
});
|
||||
|
||||
conn.on('timeout', () => {
|
||||
console.log('APNS Connection %d Timeout', conn.index);
|
||||
});
|
||||
|
||||
conn.on('disconnected', () => {
|
||||
console.log('APNS Connection %d Disconnected', conn.index);
|
||||
});
|
||||
|
||||
conn.on('socketError', () => {
|
||||
console.log('APNS Connection %d Socket Error', conn.index);
|
||||
});
|
||||
|
||||
conn.on('transmitted', function(notification, device) {
|
||||
console.log('APNS Connection %d Notification transmitted to %s', conn.index, device.token.toString('hex'));
|
||||
});
|
||||
|
||||
this.conns.push(conn);
|
||||
}
|
||||
// Sort the conn based on priority ascending, high pri first
|
||||
this.conns.sort((s1, s2) => {
|
||||
return s1.priority - s2.priority;
|
||||
});
|
||||
// Set index of conns
|
||||
for (let index = 0; index < this.conns.length; index++) {
|
||||
this.conns[index].index = index;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send apns request.
|
||||
* @param {Object} data The data we need to send, the format is the same with api request body
|
||||
* @param {Array} devices A array of devices
|
||||
* @returns {Object} A promise which is resolved immediately
|
||||
*/
|
||||
APNS.prototype.send = function(data, devices) {
|
||||
let coreData = data.data;
|
||||
let expirationTime = data['expiration_time'];
|
||||
let notification = generateNotification(coreData, expirationTime);
|
||||
for (let device of devices) {
|
||||
let qualifiedConnIndexs = chooseConns(this.conns, device);
|
||||
// We can not find a valid conn, just ignore this device
|
||||
if (qualifiedConnIndexs.length == 0) {
|
||||
continue;
|
||||
}
|
||||
let conn = this.conns[qualifiedConnIndexs[0]];
|
||||
let apnDevice = new apn.Device(device.deviceToken);
|
||||
apnDevice.connIndex = qualifiedConnIndexs[0];
|
||||
// Add additional appIdentifier info to apn device instance
|
||||
if (device.appIdentifier) {
|
||||
apnDevice.appIdentifier = device.appIdentifier;
|
||||
}
|
||||
conn.pushNotification(notification, apnDevice);
|
||||
}
|
||||
return Parse.Promise.as();
|
||||
}
|
||||
|
||||
function handleTransmissionError(conns, errCode, notification, apnDevice) {
|
||||
console.error('APNS Notification caused error: ' + errCode + ' for device ', apnDevice, notification);
|
||||
// This means the error notification is not in the cache anymore or the recepient is missing,
|
||||
// we just ignore this case
|
||||
if (!notification || !apnDevice) {
|
||||
return
|
||||
}
|
||||
|
||||
// If currentConn can not send the push notification, we try to use the next available conn.
|
||||
// Since conns is sorted by priority, the next conn means the next low pri conn.
|
||||
// If there is no conn available, we give up on sending the notification to that device.
|
||||
let qualifiedConnIndexs = chooseConns(conns, apnDevice);
|
||||
let currentConnIndex = apnDevice.connIndex;
|
||||
|
||||
let newConnIndex = -1;
|
||||
// Find the next element of currentConnIndex in qualifiedConnIndexs
|
||||
for (let index = 0; index < qualifiedConnIndexs.length - 1; index++) {
|
||||
if (qualifiedConnIndexs[index] === currentConnIndex) {
|
||||
newConnIndex = qualifiedConnIndexs[index + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
// There is no more available conns, we give up in this case
|
||||
if (newConnIndex < 0 || newConnIndex >= conns.length) {
|
||||
console.log('APNS can not find vaild connection for %j', apnDevice.token);
|
||||
return;
|
||||
}
|
||||
|
||||
let newConn = conns[newConnIndex];
|
||||
// Update device conn info
|
||||
apnDevice.connIndex = newConnIndex;
|
||||
// Use the new conn to send the notification
|
||||
newConn.pushNotification(notification, apnDevice);
|
||||
}
|
||||
|
||||
function chooseConns(conns, device) {
|
||||
// If device does not have appIdentifier, all conns maybe proper connections.
|
||||
// Otherwise we try to match the appIdentifier with bundleId
|
||||
let qualifiedConns = [];
|
||||
for (let index = 0; index < conns.length; index++) {
|
||||
let conn = conns[index];
|
||||
// If the device we need to send to does not have
|
||||
// appIdentifier, any conn could be a qualified connection
|
||||
if (!device.appIdentifier || device.appIdentifier === '') {
|
||||
qualifiedConns.push(index);
|
||||
continue;
|
||||
}
|
||||
if (device.appIdentifier === conn.bundleId) {
|
||||
qualifiedConns.push(index);
|
||||
}
|
||||
}
|
||||
return qualifiedConns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the apns notification from the data we get from api request.
|
||||
* @param {Object} coreData The data field under api request body
|
||||
* @returns {Object} A apns notification
|
||||
*/
|
||||
function generateNotification(coreData, expirationTime) {
|
||||
let notification = new apn.notification();
|
||||
let payload = {};
|
||||
for (let key in coreData) {
|
||||
switch (key) {
|
||||
case 'alert':
|
||||
notification.setAlertText(coreData.alert);
|
||||
break;
|
||||
case 'badge':
|
||||
notification.badge = coreData.badge;
|
||||
break;
|
||||
case 'sound':
|
||||
notification.sound = coreData.sound;
|
||||
break;
|
||||
case 'content-available':
|
||||
notification.setNewsstandAvailable(true);
|
||||
let isAvailable = coreData['content-available'] === 1;
|
||||
notification.setContentAvailable(isAvailable);
|
||||
break;
|
||||
case 'category':
|
||||
notification.category = coreData.category;
|
||||
break;
|
||||
default:
|
||||
payload[key] = coreData[key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
notification.payload = payload;
|
||||
notification.expiry = expirationTime;
|
||||
return notification;
|
||||
}
|
||||
|
||||
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
|
||||
APNS.generateNotification = generateNotification;
|
||||
APNS.chooseConns = chooseConns;
|
||||
APNS.handleTransmissionError = handleTransmissionError;
|
||||
}
|
||||
module.exports = APNS;
|
||||
@@ -0,0 +1,38 @@
|
||||
export function loadAdapter(adapter, defaultAdapter, options) {
|
||||
if (!adapter)
|
||||
{
|
||||
if (!defaultAdapter) {
|
||||
return options;
|
||||
}
|
||||
// Load from the default adapter when no adapter is set
|
||||
return loadAdapter(defaultAdapter, undefined, options);
|
||||
} else if (typeof adapter === "function") {
|
||||
try {
|
||||
return adapter(options);
|
||||
} catch(e) {
|
||||
if (e.name === 'TypeError') {
|
||||
var Adapter = adapter;
|
||||
return new Adapter(options);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
} else if (typeof adapter === "string") {
|
||||
adapter = require(adapter);
|
||||
// If it's define as a module, get the default
|
||||
if (adapter.default) {
|
||||
adapter = adapter.default;
|
||||
}
|
||||
return loadAdapter(adapter, undefined, options);
|
||||
} else if (adapter.module) {
|
||||
return loadAdapter(adapter.module, undefined, adapter.options);
|
||||
} else if (adapter.class) {
|
||||
return loadAdapter(adapter.class, undefined, adapter.options);
|
||||
} else if (adapter.adapter) {
|
||||
return loadAdapter(adapter.adapter, undefined, adapter.options);
|
||||
}
|
||||
// return the adapter as provided
|
||||
return adapter;
|
||||
}
|
||||
|
||||
export default loadAdapter;
|
||||
@@ -0,0 +1,23 @@
|
||||
|
||||
/*
|
||||
Mail Adapter prototype
|
||||
A MailAdapter should implement at least sendMail()
|
||||
*/
|
||||
export class MailAdapter {
|
||||
/*
|
||||
* A method for sending mail
|
||||
* @param options would have the parameters
|
||||
* - to: the recipient
|
||||
* - text: the raw text of the message
|
||||
* - subject: the subject of the email
|
||||
*/
|
||||
sendMail(options) {}
|
||||
|
||||
/* You can implement those methods if you want
|
||||
* to provide HTML templates etc...
|
||||
*/
|
||||
// sendVerificationEmail({ link, appName, user }) {}
|
||||
// sendPasswordResetEmail({ link, appName, user }) {}
|
||||
}
|
||||
|
||||
export default MailAdapter;
|
||||
@@ -8,15 +8,25 @@
|
||||
// * getFileLocation(config, request, filename)
|
||||
//
|
||||
// Default is GridStoreAdapter, which requires mongo
|
||||
// and for the API server to be using the ExportAdapter
|
||||
// and for the API server to be using the DatabaseController with Mongo
|
||||
// database adapter.
|
||||
|
||||
export class FilesAdapter {
|
||||
createFile(config, filename, data) { }
|
||||
/* this method is responsible to store the file in order to be retrived later by it's file name
|
||||
*
|
||||
* @param filename the filename to save
|
||||
* @param data the buffer of data from the file
|
||||
* @param contentType the supposed contentType
|
||||
* @discussion the contentType can be undefined if the controller was not able to determine it
|
||||
*
|
||||
* @return a promise that should fail if the storage didn't succeed
|
||||
*
|
||||
*/
|
||||
createFile(filename: string, data, contentType: string) { }
|
||||
|
||||
deleteFile(config, filename) { }
|
||||
deleteFile(filename) { }
|
||||
|
||||
getFileData(config, filename) { }
|
||||
getFileData(filename) { }
|
||||
|
||||
getFileLocation(config, filename) { }
|
||||
}
|
||||
|
||||
@@ -1,28 +1,47 @@
|
||||
// GridStoreAdapter
|
||||
//
|
||||
// Stores files in Mongo using GridStore
|
||||
// Requires the database adapter to be based on mongoclient
|
||||
/**
|
||||
GridStoreAdapter
|
||||
Stores files in Mongo using GridStore
|
||||
Requires the database adapter to be based on mongoclient
|
||||
|
||||
import { GridStore } from 'mongodb';
|
||||
@flow weak
|
||||
*/
|
||||
|
||||
import { MongoClient, GridStore, Db} from 'mongodb';
|
||||
import { FilesAdapter } from './FilesAdapter';
|
||||
|
||||
export class GridStoreAdapter extends FilesAdapter {
|
||||
_databaseURI: string;
|
||||
_connectionPromise: Promise<Db>;
|
||||
|
||||
constructor(mongoDatabaseURI: string) {
|
||||
super();
|
||||
this._databaseURI = mongoDatabaseURI;
|
||||
this._connect();
|
||||
}
|
||||
|
||||
_connect() {
|
||||
if (!this._connectionPromise) {
|
||||
this._connectionPromise = MongoClient.connect(this._databaseURI);
|
||||
}
|
||||
return this._connectionPromise;
|
||||
}
|
||||
|
||||
// For a given config object, filename, and data, store a file
|
||||
// Returns a promise
|
||||
createFile(config, filename, data) {
|
||||
return config.database.connect().then(() => {
|
||||
let gridStore = new GridStore(config.database.db, filename, 'w');
|
||||
createFile(filename: string, data, contentType) {
|
||||
return this._connect().then(database => {
|
||||
let gridStore = new GridStore(database, filename, 'w');
|
||||
return gridStore.open();
|
||||
}).then((gridStore) => {
|
||||
}).then(gridStore => {
|
||||
return gridStore.write(data);
|
||||
}).then((gridStore) => {
|
||||
}).then(gridStore => {
|
||||
return gridStore.close();
|
||||
});
|
||||
}
|
||||
|
||||
deleteFile(config, filename) {
|
||||
return config.database.connect().then(() => {
|
||||
let gridStore = new GridStore(config.database.db, filename, 'w');
|
||||
deleteFile(filename: string) {
|
||||
return this._connect().then(database => {
|
||||
let gridStore = new GridStore(database, filename, 'w');
|
||||
return gridStore.open();
|
||||
}).then((gridStore) => {
|
||||
return gridStore.unlink();
|
||||
@@ -31,13 +50,14 @@ export class GridStoreAdapter extends FilesAdapter {
|
||||
});
|
||||
}
|
||||
|
||||
getFileData(config, filename) {
|
||||
return config.database.connect().then(() => {
|
||||
return GridStore.exist(config.database.db, filename);
|
||||
}).then(() => {
|
||||
let gridStore = new GridStore(config.database.db, filename, 'r');
|
||||
return gridStore.open();
|
||||
}).then((gridStore) => {
|
||||
getFileData(filename: string) {
|
||||
return this._connect().then(database => {
|
||||
return GridStore.exist(database, filename)
|
||||
.then(() => {
|
||||
let gridStore = new GridStore(database, filename, 'r');
|
||||
return gridStore.open();
|
||||
});
|
||||
}).then(gridStore => {
|
||||
return gridStore.read();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
// S3Adapter
|
||||
//
|
||||
// Stores Parse files in AWS S3.
|
||||
|
||||
import * as AWS from 'aws-sdk';
|
||||
import { FilesAdapter } from './FilesAdapter';
|
||||
|
||||
const DEFAULT_S3_REGION = "us-east-1";
|
||||
const DEFAULT_S3_BUCKET = "parse-files";
|
||||
|
||||
export class S3Adapter extends FilesAdapter {
|
||||
// Creates an S3 session.
|
||||
// Providing AWS access and secret keys is mandatory
|
||||
// Region and bucket will use sane defaults if omitted
|
||||
constructor(
|
||||
accessKey,
|
||||
secretKey,
|
||||
{ region = DEFAULT_S3_REGION,
|
||||
bucket = DEFAULT_S3_BUCKET,
|
||||
bucketPrefix = '',
|
||||
directAccess = false } = {}
|
||||
) {
|
||||
super();
|
||||
|
||||
this._region = region;
|
||||
this._bucket = bucket;
|
||||
this._bucketPrefix = bucketPrefix;
|
||||
this._directAccess = directAccess;
|
||||
|
||||
let s3Options = {
|
||||
accessKeyId: accessKey,
|
||||
secretAccessKey: secretKey,
|
||||
params: { Bucket: this._bucket }
|
||||
};
|
||||
AWS.config._region = this._region;
|
||||
this._s3Client = new AWS.S3(s3Options);
|
||||
}
|
||||
|
||||
// For a given config object, filename, and data, store a file in S3
|
||||
// Returns a promise containing the S3 object creation response
|
||||
createFile(config, filename, data) {
|
||||
let params = {
|
||||
Key: this._bucketPrefix + filename,
|
||||
Body: data
|
||||
};
|
||||
if (this._directAccess) {
|
||||
params.ACL = "public-read"
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this._s3Client.upload(params, (err, data) => {
|
||||
if (err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
deleteFile(config, filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let params = {
|
||||
Key: this._bucketPrefix + filename
|
||||
};
|
||||
this._s3Client.deleteObject(params, (err, data) =>{
|
||||
if(err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Search for and return a file if found by filename
|
||||
// Returns a promise that succeeds with the buffer result from S3
|
||||
getFileData(config, filename) {
|
||||
let params = {Key: this._bucketPrefix + filename};
|
||||
return new Promise((resolve, reject) => {
|
||||
this._s3Client.getObject(params, (err, data) => {
|
||||
if (err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data.Body);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Generates and returns the location of a file stored in S3 for the given request and filename
|
||||
// The location is the direct S3 link if the option is set, otherwise we serve the file through parse-server
|
||||
getFileLocation(config, filename) {
|
||||
if (this._directAccess) {
|
||||
return `https://${this._bucket}.s3.amazonaws.com/${this._bucketPrefix + filename}`;
|
||||
}
|
||||
return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename));
|
||||
}
|
||||
}
|
||||
|
||||
export default S3Adapter;
|
||||
@@ -1,7 +1,7 @@
|
||||
// Logger
|
||||
//
|
||||
// Wrapper around Winston logging library with custom query
|
||||
//
|
||||
//
|
||||
// expected log entry to be in the shape of:
|
||||
// {"level":"info","message":"Your Message","timestamp":"2016-02-04T05:59:27.412Z"}
|
||||
//
|
||||
@@ -9,6 +9,7 @@ import { LoggerAdapter } from './LoggerAdapter';
|
||||
import winston from 'winston';
|
||||
import fs from 'fs';
|
||||
import { Parse } from 'parse/node';
|
||||
import { logger, configure } from '../../logger';
|
||||
|
||||
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
|
||||
const CACHE_TIME = 1000 * 60;
|
||||
@@ -57,24 +58,6 @@ let _hasValidCache = (from, until, level) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
// renews transports to current date
|
||||
let _renewTransports = ({infoLogger, errorLogger, logsFolder}) => {
|
||||
if (infoLogger) {
|
||||
infoLogger.add(winston.transports.File, {
|
||||
filename: logsFolder + _getFileName() + '.info',
|
||||
name: 'info-file',
|
||||
level: 'info'
|
||||
});
|
||||
}
|
||||
if (errorLogger) {
|
||||
errorLogger.add(winston.transports.File, {
|
||||
filename: logsFolder + _getFileName() + '.error',
|
||||
name: 'error-file',
|
||||
level: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// check that log entry has valid time stamp based on query
|
||||
let _isValidLogEntry = (from, until, entry) => {
|
||||
var _entry = JSON.parse(entry),
|
||||
@@ -84,140 +67,51 @@ let _isValidLogEntry = (from, until, entry) => {
|
||||
: false
|
||||
};
|
||||
|
||||
// ensure that file name is up to date
|
||||
let _verifyTransports = ({infoLogger, errorLogger, logsFolder}) => {
|
||||
if (_getNearestDay(currentDate) !== _getNearestDay(new Date())) {
|
||||
currentDate = new Date();
|
||||
if (infoLogger) {
|
||||
infoLogger.remove('info-file');
|
||||
}
|
||||
if (errorLogger) {
|
||||
errorLogger.remove('error-file');
|
||||
}
|
||||
_renewTransports({infoLogger, errorLogger, logsFolder});
|
||||
}
|
||||
}
|
||||
|
||||
export class FileLoggerAdapter extends LoggerAdapter {
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
|
||||
this._logsFolder = options.logsFolder || LOGS_FOLDER;
|
||||
|
||||
// check logs folder exists
|
||||
if (!fs.existsSync(this._logsFolder)) {
|
||||
fs.mkdirSync(this._logsFolder);
|
||||
}
|
||||
|
||||
this._errorLogger = new (winston.Logger)({
|
||||
exitOnError: false,
|
||||
transports: [
|
||||
new (winston.transports.File)({
|
||||
filename: this._logsFolder + _getFileName() + '.error',
|
||||
name: 'error-file',
|
||||
level: 'error'
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
this._infoLogger = new (winston.Logger)({
|
||||
exitOnError: false,
|
||||
transports: [
|
||||
new (winston.transports.File)({
|
||||
filename: this._logsFolder + _getFileName() + '.info',
|
||||
name: 'info-file',
|
||||
level: 'info'
|
||||
})
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
info() {
|
||||
_verifyTransports({infoLogger: this._infoLogger, logsFolder: this._logsFolder});
|
||||
return this._infoLogger.info.apply(undefined, arguments);
|
||||
return logger.info.apply(undefined, arguments);
|
||||
}
|
||||
|
||||
error() {
|
||||
_verifyTransports({errorLogger: this._errorLogger, logsFolder: this._logsFolder});
|
||||
return this._errorLogger.error.apply(undefined, arguments);
|
||||
return logger.error.apply(undefined, arguments);
|
||||
}
|
||||
|
||||
// custom query as winston is currently limited
|
||||
query(options, callback) {
|
||||
query(options, callback = () => {}) {
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
// defaults to 7 days prior
|
||||
let from = options.from || new Date(Date.now() - (7 * MILLISECONDS_IN_A_DAY));
|
||||
let until = options.until || new Date();
|
||||
let size = options.size || 10;
|
||||
let limit = options.size || 10;
|
||||
let order = options.order || 'desc';
|
||||
let level = options.level || 'info';
|
||||
let roundedUntil = _getNearestDay(until);
|
||||
let roundedFrom = _getNearestDay(from);
|
||||
|
||||
if (_hasValidCache(roundedFrom, roundedUntil, level)) {
|
||||
let logs = [];
|
||||
if (order !== simpleCache.order) {
|
||||
// reverse order of data
|
||||
simpleCache.data.forEach((entry) => {
|
||||
logs.unshift(entry);
|
||||
});
|
||||
} else {
|
||||
logs = simpleCache.data;
|
||||
}
|
||||
callback(logs.slice(0, size));
|
||||
return;
|
||||
}
|
||||
var options = {
|
||||
from,
|
||||
until,
|
||||
limit,
|
||||
order
|
||||
};
|
||||
|
||||
let curDate = roundedUntil;
|
||||
let curSize = 0;
|
||||
let method = order === 'desc' ? 'push' : 'unshift';
|
||||
let files = [];
|
||||
let promises = [];
|
||||
|
||||
// current a batch call, all files with valid dates are read
|
||||
while (curDate >= from) {
|
||||
files[method](this._logsFolder + curDate.toISOString() + '.' + level);
|
||||
curDate = _getPrevDay(curDate);
|
||||
}
|
||||
|
||||
// read each file and split based on newline char.
|
||||
// limitation is message cannot contain newline
|
||||
// TODO: strip out delimiter from logged message
|
||||
files.forEach(function(file, i) {
|
||||
let promise = new Parse.Promise();
|
||||
fs.readFile(file, 'utf8', function(err, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.query(options, (err, res) => {
|
||||
if (err) {
|
||||
promise.resolve([]);
|
||||
} else {
|
||||
let results = data.split('\n').filter((value) => {
|
||||
return value.trim() !== '';
|
||||
});
|
||||
promise.resolve(results);
|
||||
callback(err);
|
||||
return reject(err);
|
||||
}
|
||||
});
|
||||
promises[method](promise);
|
||||
});
|
||||
|
||||
Parse.Promise.when(promises).then((results) => {
|
||||
let logs = [];
|
||||
results.forEach(function(logEntries, i) {
|
||||
logEntries.forEach(function(entry) {
|
||||
if (_isValidLogEntry(from, until, entry)) {
|
||||
logs[method](JSON.parse(entry));
|
||||
}
|
||||
});
|
||||
});
|
||||
simpleCache = {
|
||||
timestamp: new Date(),
|
||||
from: roundedFrom,
|
||||
until: roundedUntil,
|
||||
data: logs,
|
||||
order,
|
||||
level,
|
||||
};
|
||||
callback(logs.slice(0, size));
|
||||
if (level == 'error') {
|
||||
callback(res['parse-server-error']);
|
||||
resolve(res['parse-server-error']);
|
||||
} else {
|
||||
callback(res['parse-server']);
|
||||
resolve(res['parse-server']);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
"use strict";
|
||||
// ParsePushAdapter is the default implementation of
|
||||
// PushAdapter, it uses GCM for android push and APNS
|
||||
// for ios push.
|
||||
|
||||
const Parse = require('parse/node').Parse;
|
||||
var deepcopy = require('deepcopy');
|
||||
|
||||
function OneSignalPushAdapter(pushConfig) {
|
||||
this.https = require('https');
|
||||
|
||||
this.validPushTypes = ['ios', 'android'];
|
||||
this.senderMap = {};
|
||||
|
||||
pushConfig = pushConfig || {};
|
||||
this.OneSignalConfig = {};
|
||||
this.OneSignalConfig['appId'] = pushConfig['oneSignalAppId'];
|
||||
this.OneSignalConfig['apiKey'] = pushConfig['oneSignalApiKey'];
|
||||
|
||||
this.senderMap['ios'] = this.sendToAPNS.bind(this);
|
||||
this.senderMap['android'] = this.sendToGCM.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of valid push types.
|
||||
* @returns {Array} An array of valid push types
|
||||
*/
|
||||
OneSignalPushAdapter.prototype.getValidPushTypes = function() {
|
||||
return this.validPushTypes;
|
||||
}
|
||||
|
||||
OneSignalPushAdapter.prototype.send = function(data, installations) {
|
||||
console.log("Sending notification to "+installations.length+" devices.")
|
||||
let deviceMap = classifyInstallation(installations, this.validPushTypes);
|
||||
|
||||
let sendPromises = [];
|
||||
for (let pushType in deviceMap) {
|
||||
let sender = this.senderMap[pushType];
|
||||
if (!sender) {
|
||||
console.log('Can not find sender for push type %s, %j', pushType, data);
|
||||
continue;
|
||||
}
|
||||
let devices = deviceMap[pushType];
|
||||
|
||||
if(devices.length > 0) {
|
||||
sendPromises.push(sender(data, devices));
|
||||
}
|
||||
}
|
||||
return Parse.Promise.when(sendPromises);
|
||||
}
|
||||
|
||||
OneSignalPushAdapter.prototype.sendToAPNS = function(data,tokens) {
|
||||
|
||||
data= deepcopy(data['data']);
|
||||
|
||||
var post = {};
|
||||
if(data['badge']) {
|
||||
if(data['badge'] == "Increment") {
|
||||
post['ios_badgeType'] = 'Increase';
|
||||
post['ios_badgeCount'] = 1;
|
||||
} else {
|
||||
post['ios_badgeType'] = 'SetTo';
|
||||
post['ios_badgeCount'] = data['badge'];
|
||||
}
|
||||
delete data['badge'];
|
||||
}
|
||||
if(data['alert']) {
|
||||
post['contents'] = {en: data['alert']};
|
||||
delete data['alert'];
|
||||
}
|
||||
if(data['sound']) {
|
||||
post['ios_sound'] = data['sound'];
|
||||
delete data['sound'];
|
||||
}
|
||||
if(data['content-available'] == 1) {
|
||||
post['content_available'] = true;
|
||||
delete data['content-available'];
|
||||
}
|
||||
post['data'] = data;
|
||||
|
||||
let promise = new Parse.Promise();
|
||||
|
||||
var chunk = 2000 // OneSignal can process 2000 devices at a time
|
||||
var tokenlength=tokens.length;
|
||||
var offset = 0
|
||||
// handle onesignal response. Start next batch if there's not an error.
|
||||
let handleResponse = function(wasSuccessful) {
|
||||
if (!wasSuccessful) {
|
||||
return promise.reject("OneSignal Error");
|
||||
}
|
||||
|
||||
if(offset >= tokenlength) {
|
||||
promise.resolve()
|
||||
} else {
|
||||
this.sendNext();
|
||||
}
|
||||
}.bind(this)
|
||||
|
||||
this.sendNext = function() {
|
||||
post['include_ios_tokens'] = [];
|
||||
tokens.slice(offset,offset+chunk).forEach(function(i) {
|
||||
post['include_ios_tokens'].push(i['deviceToken'])
|
||||
})
|
||||
offset+=chunk;
|
||||
this.sendToOneSignal(post, handleResponse);
|
||||
}.bind(this)
|
||||
|
||||
this.sendNext()
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
OneSignalPushAdapter.prototype.sendToGCM = function(data,tokens) {
|
||||
data= deepcopy(data['data']);
|
||||
|
||||
var post = {};
|
||||
|
||||
if(data['alert']) {
|
||||
post['contents'] = {en: data['alert']};
|
||||
delete data['alert'];
|
||||
}
|
||||
if(data['title']) {
|
||||
post['title'] = {en: data['title']};
|
||||
delete data['title'];
|
||||
}
|
||||
if(data['uri']) {
|
||||
post['url'] = data['uri'];
|
||||
}
|
||||
|
||||
post['data'] = data;
|
||||
|
||||
let promise = new Parse.Promise();
|
||||
|
||||
var chunk = 2000 // OneSignal can process 2000 devices at a time
|
||||
var tokenlength=tokens.length;
|
||||
var offset = 0
|
||||
// handle onesignal response. Start next batch if there's not an error.
|
||||
let handleResponse = function(wasSuccessful) {
|
||||
if (!wasSuccessful) {
|
||||
return promise.reject("OneSIgnal Error");
|
||||
}
|
||||
|
||||
if(offset >= tokenlength) {
|
||||
promise.resolve()
|
||||
} else {
|
||||
this.sendNext();
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
this.sendNext = function() {
|
||||
post['include_android_reg_ids'] = [];
|
||||
tokens.slice(offset,offset+chunk).forEach(function(i) {
|
||||
post['include_android_reg_ids'].push(i['deviceToken'])
|
||||
})
|
||||
offset+=chunk;
|
||||
this.sendToOneSignal(post, handleResponse);
|
||||
}.bind(this)
|
||||
|
||||
|
||||
this.sendNext();
|
||||
return promise;
|
||||
}
|
||||
|
||||
|
||||
OneSignalPushAdapter.prototype.sendToOneSignal = function(data, cb) {
|
||||
let headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Basic "+this.OneSignalConfig['apiKey']
|
||||
};
|
||||
let options = {
|
||||
host: "onesignal.com",
|
||||
port: 443,
|
||||
path: "/api/v1/notifications",
|
||||
method: "POST",
|
||||
headers: headers
|
||||
};
|
||||
data['app_id'] = this.OneSignalConfig['appId'];
|
||||
|
||||
let request = this.https.request(options, function(res) {
|
||||
if(res.statusCode < 299) {
|
||||
cb(true);
|
||||
} else {
|
||||
console.log('OneSignal Error');
|
||||
res.on('data', function(chunk) {
|
||||
console.log(chunk.toString())
|
||||
});
|
||||
cb(false)
|
||||
}
|
||||
});
|
||||
request.on('error', function(e) {
|
||||
console.log("Error connecting to OneSignal")
|
||||
console.log(e);
|
||||
cb(false);
|
||||
});
|
||||
request.write(JSON.stringify(data))
|
||||
request.end();
|
||||
}
|
||||
/**g
|
||||
* Classify the device token of installations based on its device type.
|
||||
* @param {Object} installations An array of installations
|
||||
* @param {Array} validPushTypes An array of valid push types(string)
|
||||
* @returns {Object} A map whose key is device type and value is an array of device
|
||||
*/
|
||||
function classifyInstallation(installations, validPushTypes) {
|
||||
// Init deviceTokenMap, create a empty array for each valid pushType
|
||||
let deviceMap = {};
|
||||
for (let validPushType of validPushTypes) {
|
||||
deviceMap[validPushType] = [];
|
||||
}
|
||||
for (let installation of installations) {
|
||||
// No deviceToken, ignore
|
||||
if (!installation.deviceToken) {
|
||||
continue;
|
||||
}
|
||||
let pushType = installation.deviceType;
|
||||
if (deviceMap[pushType]) {
|
||||
deviceMap[pushType].push({
|
||||
deviceToken: installation.deviceToken
|
||||
});
|
||||
} else {
|
||||
console.log('Unknown push type from installation %j', installation);
|
||||
}
|
||||
}
|
||||
return deviceMap;
|
||||
}
|
||||
|
||||
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
|
||||
OneSignalPushAdapter.classifyInstallation = classifyInstallation;
|
||||
}
|
||||
module.exports = OneSignalPushAdapter;
|
||||
@@ -1,88 +0,0 @@
|
||||
"use strict";
|
||||
// ParsePushAdapter is the default implementation of
|
||||
// PushAdapter, it uses GCM for android push and APNS
|
||||
// for ios push.
|
||||
|
||||
const Parse = require('parse/node').Parse;
|
||||
const GCM = require('../../GCM');
|
||||
const APNS = require('../../APNS');
|
||||
|
||||
function ParsePushAdapter(pushConfig) {
|
||||
this.validPushTypes = ['ios', 'android'];
|
||||
this.senderMap = {};
|
||||
|
||||
pushConfig = pushConfig || {};
|
||||
let pushTypes = Object.keys(pushConfig);
|
||||
for (let pushType of pushTypes) {
|
||||
if (this.validPushTypes.indexOf(pushType) < 0) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
'Push to ' + pushTypes + ' is not supported');
|
||||
}
|
||||
switch (pushType) {
|
||||
case 'ios':
|
||||
this.senderMap[pushType] = new APNS(pushConfig[pushType]);
|
||||
break;
|
||||
case 'android':
|
||||
this.senderMap[pushType] = new GCM(pushConfig[pushType]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of valid push types.
|
||||
* @returns {Array} An array of valid push types
|
||||
*/
|
||||
ParsePushAdapter.prototype.getValidPushTypes = function() {
|
||||
return this.validPushTypes;
|
||||
}
|
||||
|
||||
ParsePushAdapter.prototype.send = function(data, installations) {
|
||||
let deviceMap = classifyInstallation(installations, this.validPushTypes);
|
||||
let sendPromises = [];
|
||||
for (let pushType in deviceMap) {
|
||||
let sender = this.senderMap[pushType];
|
||||
if (!sender) {
|
||||
console.log('Can not find sender for push type %s, %j', pushType, data);
|
||||
continue;
|
||||
}
|
||||
let devices = deviceMap[pushType];
|
||||
sendPromises.push(sender.send(data, devices));
|
||||
}
|
||||
return Parse.Promise.when(sendPromises);
|
||||
}
|
||||
|
||||
/**g
|
||||
* Classify the device token of installations based on its device type.
|
||||
* @param {Object} installations An array of installations
|
||||
* @param {Array} validPushTypes An array of valid push types(string)
|
||||
* @returns {Object} A map whose key is device type and value is an array of device
|
||||
*/
|
||||
function classifyInstallation(installations, validPushTypes) {
|
||||
// Init deviceTokenMap, create a empty array for each valid pushType
|
||||
let deviceMap = {};
|
||||
for (let validPushType of validPushTypes) {
|
||||
deviceMap[validPushType] = [];
|
||||
}
|
||||
for (let installation of installations) {
|
||||
// No deviceToken, ignore
|
||||
if (!installation.deviceToken) {
|
||||
continue;
|
||||
}
|
||||
let pushType = installation.deviceType;
|
||||
if (deviceMap[pushType]) {
|
||||
deviceMap[pushType].push({
|
||||
deviceToken: installation.deviceToken,
|
||||
appIdentifier: installation.appIdentifier
|
||||
});
|
||||
} else {
|
||||
console.log('Unknown push type from installation %j', installation);
|
||||
}
|
||||
}
|
||||
return deviceMap;
|
||||
}
|
||||
|
||||
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
|
||||
ParsePushAdapter.classifyInstallation = classifyInstallation;
|
||||
}
|
||||
module.exports = ParsePushAdapter;
|
||||
@@ -4,14 +4,19 @@
|
||||
//
|
||||
// Adapter classes must implement the following functions:
|
||||
// * getValidPushTypes()
|
||||
// * send(devices, installations)
|
||||
// * send(devices, installations, pushStatus)
|
||||
//
|
||||
// Default is ParsePushAdapter, which uses GCM for
|
||||
// android push and APNS for ios push.
|
||||
export class PushAdapter {
|
||||
send(devices, installations) { }
|
||||
|
||||
getValidPushTypes() { }
|
||||
export class PushAdapter {
|
||||
send(devices, installations, pushStatus) { }
|
||||
|
||||
/**
|
||||
* Get an array of valid push types.
|
||||
* @returns {Array} An array of valid push types
|
||||
*/
|
||||
getValidPushTypes() {}
|
||||
}
|
||||
|
||||
export default PushAdapter;
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
let mongodb = require('mongodb');
|
||||
let Collection = mongodb.Collection;
|
||||
|
||||
export default class MongoCollection {
|
||||
_mongoCollection:Collection;
|
||||
|
||||
constructor(mongoCollection:Collection) {
|
||||
this._mongoCollection = mongoCollection;
|
||||
}
|
||||
|
||||
// Does a find with "smart indexing".
|
||||
// Currently this just means, if it needs a geoindex and there is
|
||||
// none, then build the geoindex.
|
||||
// This could be improved a lot but it's not clear if that's a good
|
||||
// idea. Or even if this behavior is a good idea.
|
||||
find(query, { skip, limit, sort } = {}) {
|
||||
return this._rawFind(query, { skip, limit, sort })
|
||||
.catch(error => {
|
||||
// Check for "no geoindex" error
|
||||
if (error.code != 17007 || !error.message.match(/unable to find index for .geoNear/)) {
|
||||
throw error;
|
||||
}
|
||||
// Figure out what key needs an index
|
||||
let key = error.message.match(/field=([A-Za-z_0-9]+) /)[1];
|
||||
if (!key) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var index = {};
|
||||
index[key] = '2d';
|
||||
//TODO: condiser moving index creation logic into Schema.js
|
||||
return this._mongoCollection.createIndex(index)
|
||||
// Retry, but just once.
|
||||
.then(() => this._rawFind(query, { skip, limit, sort }));
|
||||
});
|
||||
}
|
||||
|
||||
_rawFind(query, { skip, limit, sort } = {}) {
|
||||
return this._mongoCollection
|
||||
.find(query, { skip, limit, sort })
|
||||
.toArray();
|
||||
}
|
||||
|
||||
count(query, { skip, limit, sort } = {}) {
|
||||
return this._mongoCollection.count(query, { skip, limit, sort });
|
||||
}
|
||||
|
||||
// Atomically finds and updates an object based on query.
|
||||
// The result is the promise with an object that was in the database !AFTER! changes.
|
||||
// Postgres Note: Translates directly to `UPDATE * SET * ... RETURNING *`, which will return data after the change is done.
|
||||
findOneAndUpdate(query, update) {
|
||||
// arguments: query, sort, update, options(optional)
|
||||
// Setting `new` option to true makes it return the after document, not the before one.
|
||||
return this._mongoCollection.findAndModify(query, [], update, { new: true }).then(document => {
|
||||
// Value is the object where mongo returns multiple fields.
|
||||
return document.value;
|
||||
});
|
||||
}
|
||||
|
||||
insertOne(object) {
|
||||
return this._mongoCollection.insertOne(object);
|
||||
}
|
||||
|
||||
// Atomically updates data in the database for a single (first) object that matched the query
|
||||
// If there is nothing that matches the query - does insert
|
||||
// Postgres Note: `INSERT ... ON CONFLICT UPDATE` that is available since 9.5.
|
||||
upsertOne(query, update) {
|
||||
return this._mongoCollection.update(query, update, { upsert: true });
|
||||
}
|
||||
|
||||
updateOne(query, update) {
|
||||
return this._mongoCollection.updateOne(query, update);
|
||||
}
|
||||
|
||||
updateMany(query, update) {
|
||||
return this._mongoCollection.updateMany(query, update);
|
||||
}
|
||||
|
||||
deleteOne(query) {
|
||||
return this._mongoCollection.deleteOne(query);
|
||||
}
|
||||
|
||||
deleteMany(query) {
|
||||
return this._mongoCollection.deleteMany(query);
|
||||
}
|
||||
|
||||
drop() {
|
||||
return this._mongoCollection.drop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
|
||||
import MongoCollection from './MongoCollection';
|
||||
|
||||
function _mongoSchemaQueryFromNameQuery(name: string, query) {
|
||||
return _mongoSchemaObjectFromNameFields(name, query);
|
||||
}
|
||||
|
||||
function _mongoSchemaObjectFromNameFields(name: string, fields) {
|
||||
let object = { _id: name };
|
||||
if (fields) {
|
||||
Object.keys(fields).forEach(key => {
|
||||
object[key] = fields[key];
|
||||
});
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
export default class MongoSchemaCollection {
|
||||
_collection: MongoCollection;
|
||||
|
||||
constructor(collection: MongoCollection) {
|
||||
this._collection = collection;
|
||||
}
|
||||
|
||||
getAllSchemas() {
|
||||
return this._collection._rawFind({});
|
||||
}
|
||||
|
||||
findSchema(name: string) {
|
||||
return this._collection._rawFind(_mongoSchemaQueryFromNameQuery(name), { limit: 1 }).then(results => {
|
||||
return results[0];
|
||||
});
|
||||
}
|
||||
|
||||
// Atomically find and delete an object based on query.
|
||||
// The result is the promise with an object that was in the database before deleting.
|
||||
// Postgres Note: Translates directly to `DELETE * FROM ... RETURNING *`, which will return data after delete is done.
|
||||
findAndDeleteSchema(name: string) {
|
||||
// arguments: query, sort
|
||||
return this._collection._mongoCollection.findAndRemove(_mongoSchemaQueryFromNameQuery(name), []).then(document => {
|
||||
// Value is the object where mongo returns multiple fields.
|
||||
return document.value;
|
||||
});
|
||||
}
|
||||
|
||||
addSchema(name: string, fields) {
|
||||
let mongoObject = _mongoSchemaObjectFromNameFields(name, fields);
|
||||
return this._collection.insertOne(mongoObject);
|
||||
}
|
||||
|
||||
updateSchema(name: string, update) {
|
||||
return this._collection.updateOne(_mongoSchemaQueryFromNameQuery(name), update);
|
||||
}
|
||||
|
||||
upsertSchema(name: string, query: string, update) {
|
||||
return this._collection.upsertOne(_mongoSchemaQueryFromNameQuery(name, query), update);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
|
||||
import MongoCollection from './MongoCollection';
|
||||
import MongoSchemaCollection from './MongoSchemaCollection';
|
||||
import {parse as parseUrl, format as formatUrl} from '../../../vendor/mongodbUrl';
|
||||
|
||||
let mongodb = require('mongodb');
|
||||
let MongoClient = mongodb.MongoClient;
|
||||
|
||||
const MongoSchemaCollectionName = '_SCHEMA';
|
||||
|
||||
export class MongoStorageAdapter {
|
||||
// Private
|
||||
_uri: string;
|
||||
_options: Object;
|
||||
// Public
|
||||
connectionPromise;
|
||||
database;
|
||||
|
||||
constructor(uri: string, options: Object) {
|
||||
this._uri = uri;
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.connectionPromise) {
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
// parsing and re-formatting causes the auth value (if there) to get URI
|
||||
// encoded
|
||||
const encodedUri = formatUrl(parseUrl(this._uri));
|
||||
|
||||
this.connectionPromise = MongoClient.connect(encodedUri, this._options).then(database => {
|
||||
this.database = database;
|
||||
});
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
collection(name: string) {
|
||||
return this.connect().then(() => {
|
||||
return this.database.collection(name);
|
||||
});
|
||||
}
|
||||
|
||||
adaptiveCollection(name: string) {
|
||||
return this.connect()
|
||||
.then(() => this.database.collection(name))
|
||||
.then(rawCollection => new MongoCollection(rawCollection));
|
||||
}
|
||||
|
||||
schemaCollection(collectionPrefix: string) {
|
||||
return this.connect()
|
||||
.then(() => this.adaptiveCollection(collectionPrefix + MongoSchemaCollectionName))
|
||||
.then(collection => new MongoSchemaCollection(collection));
|
||||
}
|
||||
|
||||
collectionExists(name: string) {
|
||||
return this.connect().then(() => {
|
||||
return this.database.listCollections({ name: name }).toArray();
|
||||
}).then(collections => {
|
||||
return collections.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
dropCollection(name: string) {
|
||||
return this.collection(name).then(collection => collection.drop());
|
||||
}
|
||||
// Used for testing only right now.
|
||||
collectionsContaining(match: string) {
|
||||
return this.connect().then(() => {
|
||||
return this.database.collections();
|
||||
}).then(collections => {
|
||||
return collections.filter(collection => {
|
||||
if (collection.namespace.match(/\.system\./)) {
|
||||
return false;
|
||||
}
|
||||
return (collection.collectionName.indexOf(match) == 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default MongoStorageAdapter;
|
||||
module.exports = MongoStorageAdapter; // Required for tests
|
||||
+48
-20
@@ -2,15 +2,16 @@ var deepcopy = require('deepcopy');
|
||||
var Parse = require('parse/node').Parse;
|
||||
var RestQuery = require('./RestQuery');
|
||||
|
||||
var cache = require('./cache');
|
||||
import cache from './cache';
|
||||
|
||||
// An Auth object tells you who is requesting something and whether
|
||||
// the master key was used.
|
||||
// userObject is a Parse.User and can be null if there's no user.
|
||||
function Auth(config, isMaster, userObject) {
|
||||
function Auth({ config, isMaster = false, user, installationId } = {}) {
|
||||
this.config = config;
|
||||
this.installationId = installationId;
|
||||
this.isMaster = isMaster;
|
||||
this.user = userObject;
|
||||
this.user = user;
|
||||
|
||||
// Assuming a users roles won't change during a single request, we'll
|
||||
// only load them once.
|
||||
@@ -33,19 +34,19 @@ Auth.prototype.couldUpdateUserId = function(userId) {
|
||||
|
||||
// A helper to get a master-level Auth object
|
||||
function master(config) {
|
||||
return new Auth(config, true, null);
|
||||
return new Auth({ config, isMaster: true });
|
||||
}
|
||||
|
||||
// A helper to get a nobody-level Auth object
|
||||
function nobody(config) {
|
||||
return new Auth(config, false, null);
|
||||
return new Auth({ config, isMaster: false });
|
||||
}
|
||||
|
||||
// Returns a promise that resolves to an Auth object
|
||||
var getAuthForSessionToken = function(config, sessionToken) {
|
||||
var cachedUser = cache.getUser(sessionToken);
|
||||
var getAuthForSessionToken = function({ config, sessionToken, installationId } = {}) {
|
||||
var cachedUser = cache.users.get(sessionToken);
|
||||
if (cachedUser) {
|
||||
return Promise.resolve(new Auth(config, false, cachedUser));
|
||||
return Promise.resolve(new Auth({ config, isMaster: false, installationId, user: cachedUser }));
|
||||
}
|
||||
var restOptions = {
|
||||
limit: 1,
|
||||
@@ -61,13 +62,20 @@ var getAuthForSessionToken = function(config, sessionToken) {
|
||||
if (results.length !== 1 || !results[0]['user']) {
|
||||
return nobody(config);
|
||||
}
|
||||
|
||||
var now = new Date(),
|
||||
expiresAt = new Date(results[0].expiresAt.iso);
|
||||
if(expiresAt < now) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
|
||||
'Session token is expired.');
|
||||
}
|
||||
var obj = results[0]['user'];
|
||||
delete obj.password;
|
||||
obj['className'] = '_User';
|
||||
obj['sessionToken'] = sessionToken;
|
||||
var userObject = Parse.Object.fromJSON(obj);
|
||||
cache.setUser(sessionToken, userObject);
|
||||
return new Auth(config, false, userObject);
|
||||
let userObject = Parse.Object.fromJSON(obj);
|
||||
cache.users.set(sessionToken, userObject);
|
||||
return new Auth({ config, isMaster: false, installationId, user: userObject });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -80,7 +88,7 @@ Auth.prototype.getUserRoles = function() {
|
||||
return Promise.resolve(this.userRoles);
|
||||
}
|
||||
if (this.rolePromise) {
|
||||
return rolePromise;
|
||||
return this.rolePromise;
|
||||
}
|
||||
this.rolePromise = this._loadRoles();
|
||||
return this.rolePromise;
|
||||
@@ -109,8 +117,9 @@ Auth.prototype._loadRoles = function() {
|
||||
|
||||
var roleIDs = results.map(r => r.objectId);
|
||||
var promises = [Promise.resolve(roleIDs)];
|
||||
var queriedRoles = {};
|
||||
for (var role of roleIDs) {
|
||||
promises.push(this._getAllRoleNamesForId(role));
|
||||
promises.push(this._getAllRoleNamesForId(role, queriedRoles));
|
||||
}
|
||||
return Promise.all(promises).then((results) => {
|
||||
var allIDs = [];
|
||||
@@ -138,18 +147,22 @@ Auth.prototype._loadRoles = function() {
|
||||
};
|
||||
|
||||
// Given a role object id, get any other roles it is part of
|
||||
// TODO: Make recursive to support role nesting beyond 1 level deep
|
||||
Auth.prototype._getAllRoleNamesForId = function(roleID) {
|
||||
Auth.prototype._getAllRoleNamesForId = function(roleID, queriedRoles = {}) {
|
||||
// Don't need to requery this role as it is already being queried for.
|
||||
if (queriedRoles[roleID] != null) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
queriedRoles[roleID] = true;
|
||||
// As per documentation, a Role inherits AnotherRole
|
||||
// if this Role is in the roles pointer of this AnotherRole
|
||||
// Let's find all the roles where this role is in a roles relation
|
||||
var rolePointer = {
|
||||
__type: 'Pointer',
|
||||
className: '_Role',
|
||||
objectId: roleID
|
||||
};
|
||||
var restWhere = {
|
||||
'$relatedTo': {
|
||||
key: 'roles',
|
||||
object: rolePointer
|
||||
}
|
||||
'roles': rolePointer
|
||||
};
|
||||
var query = new RestQuery(this.config, master(this.config), '_Role',
|
||||
restWhere, {});
|
||||
@@ -159,7 +172,22 @@ Auth.prototype._getAllRoleNamesForId = function(roleID) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
var roleIDs = results.map(r => r.objectId);
|
||||
return Promise.resolve(roleIDs);
|
||||
|
||||
// we found a list of roles where the roleID
|
||||
// is referenced in the roles relation,
|
||||
// Get the roles where those found roles are also
|
||||
// referenced the same way
|
||||
var parentRolesPromises = roleIDs.map( (roleId) => {
|
||||
return this._getAllRoleNamesForId(roleId, queriedRoles);
|
||||
});
|
||||
parentRolesPromises.push(Promise.resolve(roleIDs));
|
||||
return Promise.all(parentRolesPromises);
|
||||
}).then(function(results){
|
||||
// Flatten
|
||||
let roleIDs = results.reduce( (memo, result) => {
|
||||
return memo.concat(result);
|
||||
}, []);
|
||||
return Promise.resolve([...new Set(roleIDs)]);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+121
-24
@@ -1,33 +1,130 @@
|
||||
// A Config object provides information about how a specific app is
|
||||
// configured.
|
||||
// mount is the URL for the root of the API; includes http, domain, etc.
|
||||
function Config(applicationId, mount) {
|
||||
var cache = require('./cache');
|
||||
var DatabaseAdapter = require('./DatabaseAdapter');
|
||||
|
||||
var cacheInfo = cache.apps[applicationId];
|
||||
this.valid = !!cacheInfo;
|
||||
if (!this.valid) {
|
||||
return;
|
||||
import cache from './cache';
|
||||
|
||||
function removeTrailingSlash(str) {
|
||||
if (!str) {
|
||||
return str;
|
||||
}
|
||||
|
||||
this.applicationId = applicationId;
|
||||
this.collectionPrefix = cacheInfo.collectionPrefix || '';
|
||||
this.masterKey = cacheInfo.masterKey;
|
||||
this.clientKey = cacheInfo.clientKey;
|
||||
this.javascriptKey = cacheInfo.javascriptKey;
|
||||
this.dotNetKey = cacheInfo.dotNetKey;
|
||||
this.restAPIKey = cacheInfo.restAPIKey;
|
||||
this.fileKey = cacheInfo.fileKey;
|
||||
this.facebookAppIds = cacheInfo.facebookAppIds;
|
||||
this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers;
|
||||
|
||||
this.database = DatabaseAdapter.getDatabaseConnection(applicationId);
|
||||
this.filesController = cacheInfo.filesController;
|
||||
|
||||
this.oauth = cacheInfo.oauth;
|
||||
this.mount = mount;
|
||||
if (str.endsWith("/")) {
|
||||
str = str.substr(0, str.length-1);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
export class Config {
|
||||
constructor(applicationId: string, mount: string) {
|
||||
let DatabaseAdapter = require('./DatabaseAdapter');
|
||||
let cacheInfo = cache.apps.get(applicationId);
|
||||
if (!cacheInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.applicationId = applicationId;
|
||||
this.masterKey = cacheInfo.masterKey;
|
||||
this.clientKey = cacheInfo.clientKey;
|
||||
this.javascriptKey = cacheInfo.javascriptKey;
|
||||
this.dotNetKey = cacheInfo.dotNetKey;
|
||||
this.restAPIKey = cacheInfo.restAPIKey;
|
||||
this.fileKey = cacheInfo.fileKey;
|
||||
this.facebookAppIds = cacheInfo.facebookAppIds;
|
||||
this.allowClientClassCreation = cacheInfo.allowClientClassCreation;
|
||||
this.database = DatabaseAdapter.getDatabaseConnection(applicationId, cacheInfo.collectionPrefix);
|
||||
|
||||
this.serverURL = cacheInfo.serverURL;
|
||||
this.publicServerURL = removeTrailingSlash(cacheInfo.publicServerURL);
|
||||
this.verifyUserEmails = cacheInfo.verifyUserEmails;
|
||||
this.appName = cacheInfo.appName;
|
||||
|
||||
this.hooksController = cacheInfo.hooksController;
|
||||
this.filesController = cacheInfo.filesController;
|
||||
this.pushController = cacheInfo.pushController;
|
||||
this.loggerController = cacheInfo.loggerController;
|
||||
this.userController = cacheInfo.userController;
|
||||
this.authDataManager = cacheInfo.authDataManager;
|
||||
this.customPages = cacheInfo.customPages || {};
|
||||
this.mount = removeTrailingSlash(mount);
|
||||
this.liveQueryController = cacheInfo.liveQueryController;
|
||||
this.sessionLength = cacheInfo.sessionLength;
|
||||
this.generateSessionExpiresAt = this.generateSessionExpiresAt.bind(this);
|
||||
}
|
||||
|
||||
static validate(options) {
|
||||
this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails,
|
||||
appName: options.appName,
|
||||
publicServerURL: options.publicServerURL})
|
||||
if (options.publicServerURL) {
|
||||
if (!options.publicServerURL.startsWith("http://") && !options.publicServerURL.startsWith("https://")) {
|
||||
throw "publicServerURL should be a valid HTTPS URL starting with https://"
|
||||
}
|
||||
}
|
||||
|
||||
this.validateSessionLength(options.sessionLength);
|
||||
}
|
||||
|
||||
static validateEmailConfiguration({verifyUserEmails, appName, publicServerURL}) {
|
||||
if (verifyUserEmails) {
|
||||
if (typeof appName !== 'string') {
|
||||
throw 'An app name is required when using email verification.';
|
||||
}
|
||||
if (typeof publicServerURL !== 'string') {
|
||||
throw 'A public server url is required when using email verification.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get mount() {
|
||||
var mount = this._mount;
|
||||
if (this.publicServerURL) {
|
||||
mount = this.publicServerURL;
|
||||
}
|
||||
return mount;
|
||||
}
|
||||
|
||||
set mount(newValue) {
|
||||
this._mount = newValue;
|
||||
}
|
||||
|
||||
static validateSessionLength(sessionLength) {
|
||||
if(isNaN(sessionLength)) {
|
||||
throw 'Session length must be a valid number.';
|
||||
}
|
||||
else if(sessionLength <= 0) {
|
||||
throw 'Session length must be a value greater than 0.'
|
||||
}
|
||||
}
|
||||
|
||||
generateSessionExpiresAt() {
|
||||
var now = new Date();
|
||||
return new Date(now.getTime() + (this.sessionLength*1000));
|
||||
}
|
||||
|
||||
get invalidLinkURL() {
|
||||
return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`;
|
||||
}
|
||||
|
||||
get verifyEmailSuccessURL() {
|
||||
return this.customPages.verifyEmailSuccess || `${this.publicServerURL}/apps/verify_email_success.html`;
|
||||
}
|
||||
|
||||
get choosePasswordURL() {
|
||||
return this.customPages.choosePassword || `${this.publicServerURL}/apps/choose_password`;
|
||||
}
|
||||
|
||||
get requestResetPasswordURL() {
|
||||
return `${this.publicServerURL}/apps/${this.applicationId}/request_password_reset`;
|
||||
}
|
||||
|
||||
get passwordResetSuccessURL() {
|
||||
return this.customPages.passwordResetSuccess || `${this.publicServerURL}/apps/password_reset_success.html`;
|
||||
}
|
||||
|
||||
get verifyEmailURL() {
|
||||
return `${this.publicServerURL}/apps/${this.applicationId}/verify_email`;
|
||||
}
|
||||
};
|
||||
|
||||
export default Config;
|
||||
module.exports = Config;
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
AdaptableController.js
|
||||
|
||||
AdaptableController is the base class for all controllers
|
||||
that support adapter,
|
||||
The super class takes care of creating the right instance for the adapter
|
||||
based on the parameters passed
|
||||
|
||||
*/
|
||||
|
||||
// _adapter is private, use Symbol
|
||||
var _adapter = Symbol();
|
||||
import Config from '../Config';
|
||||
|
||||
export class AdaptableController {
|
||||
|
||||
constructor(adapter, appId, options) {
|
||||
this.options = options;
|
||||
this.appId = appId;
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
set adapter(adapter) {
|
||||
this.validateAdapter(adapter);
|
||||
this[_adapter] = adapter;
|
||||
}
|
||||
|
||||
get adapter() {
|
||||
return this[_adapter];
|
||||
}
|
||||
|
||||
get config() {
|
||||
return new Config(this.appId);
|
||||
}
|
||||
|
||||
expectedAdapterType() {
|
||||
throw new Error("Subclasses should implement expectedAdapterType()");
|
||||
}
|
||||
|
||||
validateAdapter(adapter) {
|
||||
if (!adapter) {
|
||||
throw new Error(this.constructor.name+" requires an adapter");
|
||||
}
|
||||
|
||||
let Type = this.expectedAdapterType();
|
||||
// Allow skipping for testing
|
||||
if (!Type) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Makes sure the prototype matches
|
||||
let mismatches = Object.getOwnPropertyNames(Type.prototype).reduce( (obj, key) => {
|
||||
const adapterType = typeof adapter[key];
|
||||
const expectedType = typeof Type.prototype[key];
|
||||
if (adapterType !== expectedType) {
|
||||
obj[key] = {
|
||||
expected: expectedType,
|
||||
actual: adapterType
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
if (Object.keys(mismatches).length > 0) {
|
||||
throw new Error("Adapter prototype don't match expected prototype", adapter, mismatches);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AdaptableController;
|
||||
@@ -0,0 +1,659 @@
|
||||
// A database adapter that works with data exported from the hosted
|
||||
// Parse database.
|
||||
|
||||
import intersect from 'intersect';
|
||||
|
||||
var mongodb = require('mongodb');
|
||||
var Parse = require('parse/node').Parse;
|
||||
|
||||
var Schema = require('./../Schema');
|
||||
var transform = require('./../transform');
|
||||
const deepcopy = require('deepcopy');
|
||||
|
||||
// options can contain:
|
||||
// collectionPrefix: the string to put in front of every collection name.
|
||||
function DatabaseController(adapter, { collectionPrefix } = {}) {
|
||||
this.adapter = adapter;
|
||||
|
||||
this.collectionPrefix = collectionPrefix;
|
||||
|
||||
// We don't want a mutable this.schema, because then you could have
|
||||
// one request that uses different schemas for different parts of
|
||||
// it. Instead, use loadSchema to get a schema.
|
||||
this.schemaPromise = null;
|
||||
|
||||
this.connect();
|
||||
}
|
||||
|
||||
// Connects to the database. Returns a promise that resolves when the
|
||||
// connection is successful.
|
||||
DatabaseController.prototype.connect = function() {
|
||||
return this.adapter.connect();
|
||||
};
|
||||
|
||||
DatabaseController.prototype.adaptiveCollection = function(className) {
|
||||
return this.adapter.adaptiveCollection(this.collectionPrefix + className);
|
||||
};
|
||||
|
||||
DatabaseController.prototype.schemaCollection = function() {
|
||||
return this.adapter.schemaCollection(this.collectionPrefix);
|
||||
};
|
||||
|
||||
DatabaseController.prototype.collectionExists = function(className) {
|
||||
return this.adapter.collectionExists(this.collectionPrefix + className);
|
||||
};
|
||||
|
||||
DatabaseController.prototype.dropCollection = function(className) {
|
||||
return this.adapter.dropCollection(this.collectionPrefix + className);
|
||||
};
|
||||
|
||||
function returnsTrue() {
|
||||
return true;
|
||||
}
|
||||
|
||||
DatabaseController.prototype.validateClassName = function(className) {
|
||||
if (!Schema.classNameIsValid(className)) {
|
||||
const error = new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'invalid className: ' + className);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
// Returns a promise for a schema object.
|
||||
// If we are provided a acceptor, then we run it on the schema.
|
||||
// If the schema isn't accepted, we reload it at most once.
|
||||
DatabaseController.prototype.loadSchema = function(acceptor = returnsTrue) {
|
||||
|
||||
if (!this.schemaPromise) {
|
||||
this.schemaPromise = this.schemaCollection().then(collection => {
|
||||
delete this.schemaPromise;
|
||||
return Schema.load(collection);
|
||||
});
|
||||
return this.schemaPromise;
|
||||
}
|
||||
|
||||
return this.schemaPromise.then((schema) => {
|
||||
if (acceptor(schema)) {
|
||||
return schema;
|
||||
}
|
||||
this.schemaPromise = this.schemaCollection().then(collection => {
|
||||
delete this.schemaPromise;
|
||||
return Schema.load(collection);
|
||||
});
|
||||
return this.schemaPromise;
|
||||
});
|
||||
};
|
||||
|
||||
// Returns a promise for the classname that is related to the given
|
||||
// classname through the key.
|
||||
// TODO: make this not in the DatabaseController interface
|
||||
DatabaseController.prototype.redirectClassNameForKey = function(className, key) {
|
||||
return this.loadSchema().then((schema) => {
|
||||
var t = schema.getExpectedType(className, key);
|
||||
var match = t ? t.match(/^relation<(.*)>$/) : false;
|
||||
if (match) {
|
||||
return match[1];
|
||||
} else {
|
||||
return className;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Uses the schema to validate the object (REST API format).
|
||||
// Returns a promise that resolves to the new schema.
|
||||
// This does not update this.schema, because in a situation like a
|
||||
// batch request, that could confuse other users of the schema.
|
||||
DatabaseController.prototype.validateObject = function(className, object, query, options) {
|
||||
let schema;
|
||||
let isMaster = !('acl' in options);
|
||||
var aclGroup = options.acl || [];
|
||||
return this.loadSchema().then(s => {
|
||||
schema = s;
|
||||
if (isMaster) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this.canAddField(schema, className, object, aclGroup);
|
||||
}).then(() => {
|
||||
return schema.validateObject(className, object, query);
|
||||
});
|
||||
};
|
||||
|
||||
// Like transform.untransformObject but you need to provide a className.
|
||||
// Filters out any data that shouldn't be on this REST-formatted object.
|
||||
DatabaseController.prototype.untransformObject = function(
|
||||
schema, isMaster, aclGroup, className, mongoObject) {
|
||||
var object = transform.untransformObject(schema, className, mongoObject);
|
||||
|
||||
if (className !== '_User') {
|
||||
return object;
|
||||
}
|
||||
|
||||
if (isMaster || (aclGroup.indexOf(object.objectId) > -1)) {
|
||||
return object;
|
||||
}
|
||||
|
||||
delete object.authData;
|
||||
delete object.sessionToken;
|
||||
return object;
|
||||
};
|
||||
|
||||
// Runs an update on the database.
|
||||
// Returns a promise for an object with the new values for field
|
||||
// modifications that don't know their results ahead of time, like
|
||||
// 'increment'.
|
||||
// Options:
|
||||
// acl: a list of strings. If the object to be updated has an ACL,
|
||||
// one of the provided strings must provide the caller with
|
||||
// write permissions.
|
||||
DatabaseController.prototype.update = function(className, query, update, options) {
|
||||
|
||||
const originalUpdate = update;
|
||||
// Make a copy of the object, so we don't mutate the incoming data.
|
||||
update = deepcopy(update);
|
||||
|
||||
var acceptor = function(schema) {
|
||||
return schema.hasKeys(className, Object.keys(query));
|
||||
};
|
||||
var isMaster = !('acl' in options);
|
||||
var aclGroup = options.acl || [];
|
||||
var mongoUpdate, schema;
|
||||
return this.loadSchema(acceptor)
|
||||
.then(s => {
|
||||
schema = s;
|
||||
if (!isMaster) {
|
||||
return schema.validatePermission(className, aclGroup, 'update');
|
||||
}
|
||||
return Promise.resolve();
|
||||
})
|
||||
.then(() => this.handleRelationUpdates(className, query.objectId, update))
|
||||
.then(() => this.adaptiveCollection(className))
|
||||
.then(collection => {
|
||||
var mongoWhere = transform.transformWhere(schema, className, query);
|
||||
if (options.acl) {
|
||||
var writePerms = [
|
||||
{_wperm: {'$exists': false}}
|
||||
];
|
||||
for (var entry of options.acl) {
|
||||
writePerms.push({_wperm: {'$in': [entry]}});
|
||||
}
|
||||
mongoWhere = {'$and': [mongoWhere, {'$or': writePerms}]};
|
||||
}
|
||||
mongoUpdate = transform.transformUpdate(schema, className, update);
|
||||
return collection.findOneAndUpdate(mongoWhere, mongoUpdate);
|
||||
})
|
||||
.then(result => {
|
||||
if (!result) {
|
||||
return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Object not found.'));
|
||||
}
|
||||
return sanitizeDatabaseResult(originalUpdate, result);
|
||||
});
|
||||
};
|
||||
|
||||
function sanitizeDatabaseResult(originalObject, result) {
|
||||
let response = {};
|
||||
if (!result) {
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
Object.keys(originalObject).forEach(key => {
|
||||
let keyUpdate = originalObject[key];
|
||||
// determine if that was an op
|
||||
if (keyUpdate && typeof keyUpdate === 'object' && keyUpdate.__op
|
||||
&& ['Add', 'AddUnique', 'Remove', 'Increment'].indexOf(keyUpdate.__op) > -1) {
|
||||
// only valid ops that produce an actionable result
|
||||
response[key] = result[key];
|
||||
}
|
||||
});
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
|
||||
// Processes relation-updating operations from a REST-format update.
|
||||
// Returns a promise that resolves successfully when these are
|
||||
// processed.
|
||||
// This mutates update.
|
||||
DatabaseController.prototype.handleRelationUpdates = function(className,
|
||||
objectId,
|
||||
update) {
|
||||
var pending = [];
|
||||
var deleteMe = [];
|
||||
objectId = update.objectId || objectId;
|
||||
|
||||
var process = (op, key) => {
|
||||
if (!op) {
|
||||
return;
|
||||
}
|
||||
if (op.__op == 'AddRelation') {
|
||||
for (var object of op.objects) {
|
||||
pending.push(this.addRelation(key, className,
|
||||
objectId,
|
||||
object.objectId));
|
||||
}
|
||||
deleteMe.push(key);
|
||||
}
|
||||
|
||||
if (op.__op == 'RemoveRelation') {
|
||||
for (var object of op.objects) {
|
||||
pending.push(this.removeRelation(key, className,
|
||||
objectId,
|
||||
object.objectId));
|
||||
}
|
||||
deleteMe.push(key);
|
||||
}
|
||||
|
||||
if (op.__op == 'Batch') {
|
||||
for (var x of op.ops) {
|
||||
process(x, key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (var key in update) {
|
||||
process(update[key], key);
|
||||
}
|
||||
for (var key of deleteMe) {
|
||||
delete update[key];
|
||||
}
|
||||
return Promise.all(pending);
|
||||
};
|
||||
|
||||
// Adds a relation.
|
||||
// Returns a promise that resolves successfully iff the add was successful.
|
||||
DatabaseController.prototype.addRelation = function(key, fromClassName, fromId, toId) {
|
||||
let doc = {
|
||||
relatedId: toId,
|
||||
owningId : fromId
|
||||
};
|
||||
let className = `_Join:${key}:${fromClassName}`;
|
||||
return this.adaptiveCollection(className).then((coll) => {
|
||||
return coll.upsertOne(doc, doc);
|
||||
});
|
||||
};
|
||||
|
||||
// Removes a relation.
|
||||
// Returns a promise that resolves successfully iff the remove was
|
||||
// successful.
|
||||
DatabaseController.prototype.removeRelation = function(key, fromClassName, fromId, toId) {
|
||||
var doc = {
|
||||
relatedId: toId,
|
||||
owningId: fromId
|
||||
};
|
||||
let className = `_Join:${key}:${fromClassName}`;
|
||||
return this.adaptiveCollection(className).then(coll => {
|
||||
return coll.deleteOne(doc);
|
||||
});
|
||||
};
|
||||
|
||||
// Removes objects matches this query from the database.
|
||||
// Returns a promise that resolves successfully iff the object was
|
||||
// deleted.
|
||||
// Options:
|
||||
// acl: a list of strings. If the object to be updated has an ACL,
|
||||
// one of the provided strings must provide the caller with
|
||||
// write permissions.
|
||||
DatabaseController.prototype.destroy = function(className, query, options = {}) {
|
||||
var isMaster = !('acl' in options);
|
||||
var aclGroup = options.acl || [];
|
||||
|
||||
var schema;
|
||||
return this.loadSchema()
|
||||
.then(s => {
|
||||
schema = s;
|
||||
if (!isMaster) {
|
||||
return schema.validatePermission(className, aclGroup, 'delete');
|
||||
}
|
||||
return Promise.resolve();
|
||||
})
|
||||
.then(() => this.adaptiveCollection(className))
|
||||
.then(collection => {
|
||||
let mongoWhere = transform.transformWhere(schema, className, query);
|
||||
|
||||
if (options.acl) {
|
||||
var writePerms = [
|
||||
{ _wperm: { '$exists': false } }
|
||||
];
|
||||
for (var entry of options.acl) {
|
||||
writePerms.push({ _wperm: { '$in': [entry] } });
|
||||
}
|
||||
mongoWhere = { '$and': [mongoWhere, { '$or': writePerms }] };
|
||||
}
|
||||
return collection.deleteMany(mongoWhere);
|
||||
})
|
||||
.then(resp => {
|
||||
//Check _Session to avoid changing password failed without any session.
|
||||
// TODO: @nlutsenko Stop relying on `result.n`
|
||||
if (resp.result.n === 0 && className !== "_Session") {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Inserts an object into the database.
|
||||
// Returns a promise that resolves successfully iff the object saved.
|
||||
DatabaseController.prototype.create = function(className, object, options) {
|
||||
// Make a copy of the object, so we don't mutate the incoming data.
|
||||
let originalObject = object;
|
||||
object = deepcopy(object);
|
||||
|
||||
var schema;
|
||||
var isMaster = !('acl' in options);
|
||||
var aclGroup = options.acl || [];
|
||||
|
||||
return this.validateClassName(className)
|
||||
.then(() => this.loadSchema())
|
||||
.then(s => {
|
||||
schema = s;
|
||||
if (!isMaster) {
|
||||
return schema.validatePermission(className, aclGroup, 'create');
|
||||
}
|
||||
return Promise.resolve();
|
||||
})
|
||||
.then(() => this.handleRelationUpdates(className, null, object))
|
||||
.then(() => this.adaptiveCollection(className))
|
||||
.then(coll => {
|
||||
var mongoObject = transform.transformCreate(schema, className, object);
|
||||
return coll.insertOne(mongoObject);
|
||||
})
|
||||
.then(result => {
|
||||
return sanitizeDatabaseResult(originalObject, result.ops[0]);
|
||||
});
|
||||
};
|
||||
|
||||
DatabaseController.prototype.canAddField = function(schema, className, object, aclGroup) {
|
||||
let classSchema = schema.data[className];
|
||||
if (!classSchema) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
let fields = Object.keys(object);
|
||||
let schemaFields = Object.keys(classSchema);
|
||||
let newKeys = fields.filter((field) => {
|
||||
return schemaFields.indexOf(field) < 0;
|
||||
})
|
||||
if (newKeys.length > 0) {
|
||||
return schema.validatePermission(className, aclGroup, 'addField');
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Runs a mongo query on the database.
|
||||
// This should only be used for testing - use 'find' for normal code
|
||||
// to avoid Mongo-format dependencies.
|
||||
// Returns a promise that resolves to a list of items.
|
||||
DatabaseController.prototype.mongoFind = function(className, query, options = {}) {
|
||||
return this.adaptiveCollection(className)
|
||||
.then(collection => collection.find(query, options));
|
||||
};
|
||||
|
||||
// Deletes everything in the database matching the current collectionPrefix
|
||||
// Won't delete collections in the system namespace
|
||||
// Returns a promise.
|
||||
DatabaseController.prototype.deleteEverything = function() {
|
||||
this.schemaPromise = null;
|
||||
|
||||
return this.adapter.collectionsContaining(this.collectionPrefix).then(collections => {
|
||||
let promises = collections.map(collection => {
|
||||
return collection.drop();
|
||||
});
|
||||
return Promise.all(promises);
|
||||
});
|
||||
};
|
||||
|
||||
// Finds the keys in a query. Returns a Set. REST format only
|
||||
function keysForQuery(query) {
|
||||
var sublist = query['$and'] || query['$or'];
|
||||
if (sublist) {
|
||||
let answer = sublist.reduce((memo, subquery) => {
|
||||
return memo.concat(keysForQuery(subquery));
|
||||
}, []);
|
||||
|
||||
return new Set(answer);
|
||||
}
|
||||
|
||||
return new Set(Object.keys(query));
|
||||
}
|
||||
|
||||
// Returns a promise for a list of related ids given an owning id.
|
||||
// className here is the owning className.
|
||||
DatabaseController.prototype.relatedIds = function(className, key, owningId) {
|
||||
return this.adaptiveCollection(joinTableName(className, key))
|
||||
.then(coll => coll.find({owningId : owningId}))
|
||||
.then(results => results.map(r => r.relatedId));
|
||||
};
|
||||
|
||||
// Returns a promise for a list of owning ids given some related ids.
|
||||
// className here is the owning className.
|
||||
DatabaseController.prototype.owningIds = function(className, key, relatedIds) {
|
||||
return this.adaptiveCollection(joinTableName(className, key))
|
||||
.then(coll => coll.find({ relatedId: { '$in': relatedIds } }))
|
||||
.then(results => results.map(r => r.owningId));
|
||||
};
|
||||
|
||||
// Modifies query so that it no longer has $in on relation fields, or
|
||||
// equal-to-pointer constraints on relation fields.
|
||||
// Returns a promise that resolves when query is mutated
|
||||
DatabaseController.prototype.reduceInRelation = function(className, query, schema) {
|
||||
|
||||
// Search for an in-relation or equal-to-relation
|
||||
// Make it sequential for now, not sure of paralleization side effects
|
||||
if (query['$or']) {
|
||||
let ors = query['$or'];
|
||||
return Promise.all(ors.map((aQuery, index) => {
|
||||
return this.reduceInRelation(className, aQuery, schema).then((aQuery) => {
|
||||
query['$or'][index] = aQuery;
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
let promises = Object.keys(query).map((key) => {
|
||||
if (query[key] && (query[key]['$in'] || query[key]['$ne'] || query[key]['$nin'] || query[key].__type == 'Pointer')) {
|
||||
let t = schema.getExpectedType(className, key);
|
||||
let match = t ? t.match(/^relation<(.*)>$/) : false;
|
||||
if (!match) {
|
||||
return Promise.resolve(query);
|
||||
}
|
||||
let relatedClassName = match[1];
|
||||
// Build the list of queries
|
||||
let queries = Object.keys(query[key]).map((constraintKey) => {
|
||||
let relatedIds;
|
||||
let isNegation = false;
|
||||
if (constraintKey === 'objectId') {
|
||||
relatedIds = [query[key].objectId];
|
||||
} else if (constraintKey == '$in') {
|
||||
relatedIds = query[key]['$in'].map(r => r.objectId);
|
||||
} else if (constraintKey == '$nin') {
|
||||
isNegation = true;
|
||||
relatedIds = query[key]['$nin'].map(r => r.objectId);
|
||||
} else if (constraintKey == '$ne') {
|
||||
isNegation = true;
|
||||
relatedIds = [query[key]['$ne'].objectId];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
isNegation,
|
||||
relatedIds
|
||||
}
|
||||
});
|
||||
|
||||
// remove the current queryKey as we don,t need it anymore
|
||||
delete query[key];
|
||||
// execute each query independnently to build the list of
|
||||
// $in / $nin
|
||||
let promises = queries.map((q) => {
|
||||
if (!q) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this.owningIds(className, key, q.relatedIds).then((ids) => {
|
||||
if (q.isNegation) {
|
||||
this.addNotInObjectIdsIds(ids, query);
|
||||
} else {
|
||||
this.addInObjectIdsIds(ids, query);
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
return Promise.resolve();
|
||||
})
|
||||
|
||||
}
|
||||
return Promise.resolve();
|
||||
})
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
return Promise.resolve(query);
|
||||
})
|
||||
};
|
||||
|
||||
// Modifies query so that it no longer has $relatedTo
|
||||
// Returns a promise that resolves when query is mutated
|
||||
DatabaseController.prototype.reduceRelationKeys = function(className, query) {
|
||||
|
||||
if (query['$or']) {
|
||||
return Promise.all(query['$or'].map((aQuery) => {
|
||||
return this.reduceRelationKeys(className, aQuery);
|
||||
}));
|
||||
}
|
||||
|
||||
var relatedTo = query['$relatedTo'];
|
||||
if (relatedTo) {
|
||||
return this.relatedIds(
|
||||
relatedTo.object.className,
|
||||
relatedTo.key,
|
||||
relatedTo.object.objectId).then((ids) => {
|
||||
delete query['$relatedTo'];
|
||||
this.addInObjectIdsIds(ids, query);
|
||||
return this.reduceRelationKeys(className, query);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
DatabaseController.prototype.addInObjectIdsIds = function(ids = null, query) {
|
||||
let idsFromString = typeof query.objectId === 'string' ? [query.objectId] : null;
|
||||
let idsFromEq = query.objectId && query.objectId['$eq'] ? [query.objectId['$eq']] : null;
|
||||
let idsFromIn = query.objectId && query.objectId['$in'] ? query.objectId['$in'] : null;
|
||||
|
||||
let allIds = [idsFromString, idsFromEq, idsFromIn, ids].filter(list => list !== null);
|
||||
let totalLength = allIds.reduce((memo, list) => memo + list.length, 0);
|
||||
|
||||
let idsIntersection = [];
|
||||
if (totalLength > 125) {
|
||||
idsIntersection = intersect.big(allIds);
|
||||
} else {
|
||||
idsIntersection = intersect(allIds);
|
||||
}
|
||||
|
||||
// Need to make sure we don't clobber existing $lt or other constraints on objectId.
|
||||
// Clobbering $eq, $in and shorthand $eq (query.objectId === 'string') constraints
|
||||
// is expected though.
|
||||
if (!('objectId' in query) || typeof query.objectId === 'string') {
|
||||
query.objectId = {};
|
||||
}
|
||||
query.objectId['$in'] = idsIntersection;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
DatabaseController.prototype.addNotInObjectIdsIds = function(ids = null, query) {
|
||||
let idsFromNin = query.objectId && query.objectId['$nin'] ? query.objectId['$nin'] : null;
|
||||
let allIds = [idsFromNin, ids].filter(list => list !== null);
|
||||
let totalLength = allIds.reduce((memo, list) => memo + list.length, 0);
|
||||
|
||||
let idsIntersection = [];
|
||||
if (totalLength > 125) {
|
||||
idsIntersection = intersect.big(allIds);
|
||||
} else {
|
||||
idsIntersection = intersect(allIds);
|
||||
}
|
||||
|
||||
// Need to make sure we don't clobber existing $lt or other constraints on objectId.
|
||||
// Clobbering $eq, $in and shorthand $eq (query.objectId === 'string') constraints
|
||||
// is expected though.
|
||||
if (!('objectId' in query) || typeof query.objectId === 'string') {
|
||||
query.objectId = {};
|
||||
}
|
||||
query.objectId['$nin'] = idsIntersection;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
// Runs a query on the database.
|
||||
// Returns a promise that resolves to a list of items.
|
||||
// Options:
|
||||
// skip number of results to skip.
|
||||
// limit limit to this number of results.
|
||||
// sort an object where keys are the fields to sort by.
|
||||
// the value is +1 for ascending, -1 for descending.
|
||||
// count run a count instead of returning results.
|
||||
// acl restrict this operation with an ACL for the provided array
|
||||
// of user objectIds and roles. acl: null means no user.
|
||||
// when this field is not present, don't do anything regarding ACLs.
|
||||
// TODO: make userIds not needed here. The db adapter shouldn't know
|
||||
// anything about users, ideally. Then, improve the format of the ACL
|
||||
// arg to work like the others.
|
||||
DatabaseController.prototype.find = function(className, query, options = {}) {
|
||||
let mongoOptions = {};
|
||||
if (options.skip) {
|
||||
mongoOptions.skip = options.skip;
|
||||
}
|
||||
if (options.limit) {
|
||||
mongoOptions.limit = options.limit;
|
||||
}
|
||||
|
||||
let isMaster = !('acl' in options);
|
||||
let aclGroup = options.acl || [];
|
||||
let acceptor = schema => schema.hasKeys(className, keysForQuery(query))
|
||||
let schema = null;
|
||||
return this.loadSchema(acceptor).then(s => {
|
||||
schema = s;
|
||||
if (options.sort) {
|
||||
mongoOptions.sort = {};
|
||||
for (let key in options.sort) {
|
||||
let mongoKey = transform.transformKey(schema, className, key);
|
||||
mongoOptions.sort[mongoKey] = options.sort[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (!isMaster) {
|
||||
let op = typeof query.objectId == 'string' && Object.keys(query).length === 1 ?
|
||||
'get' :
|
||||
'find';
|
||||
return schema.validatePermission(className, aclGroup, op);
|
||||
}
|
||||
return Promise.resolve();
|
||||
})
|
||||
.then(() => this.reduceRelationKeys(className, query))
|
||||
.then(() => this.reduceInRelation(className, query, schema))
|
||||
.then(() => this.adaptiveCollection(className))
|
||||
.then(collection => {
|
||||
let mongoWhere = transform.transformWhere(schema, className, query);
|
||||
if (!isMaster) {
|
||||
let orParts = [
|
||||
{"_rperm" : { "$exists": false }},
|
||||
{"_rperm" : { "$in" : ["*"]}}
|
||||
];
|
||||
for (let acl of aclGroup) {
|
||||
orParts.push({"_rperm" : { "$in" : [acl]}});
|
||||
}
|
||||
mongoWhere = {'$and': [mongoWhere, {'$or': orParts}]};
|
||||
}
|
||||
if (options.count) {
|
||||
delete mongoOptions.limit;
|
||||
return collection.count(mongoWhere, mongoOptions);
|
||||
} else {
|
||||
return collection.find(mongoWhere, mongoOptions)
|
||||
.then((mongoResults) => {
|
||||
return mongoResults.map((r) => {
|
||||
return this.untransformObject(
|
||||
schema, isMaster, aclGroup, className, r);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function joinTableName(className, key) {
|
||||
return `_Join:${key}:${className}`;
|
||||
}
|
||||
|
||||
module.exports = DatabaseController;
|
||||
@@ -1,88 +1,42 @@
|
||||
// FilesController.js
|
||||
|
||||
import express from 'express';
|
||||
import mime from 'mime';
|
||||
import { Parse } from 'parse/node';
|
||||
import BodyParser from 'body-parser';
|
||||
import * as Middlewares from '../middlewares';
|
||||
import Config from '../Config';
|
||||
import { randomHexString } from '../cryptoUtils';
|
||||
import AdaptableController from './AdaptableController';
|
||||
import { FilesAdapter } from '../Adapters/Files/FilesAdapter';
|
||||
import path from 'path';
|
||||
import mime from 'mime';
|
||||
|
||||
export class FilesController {
|
||||
constructor(filesAdapter) {
|
||||
this._filesAdapter = filesAdapter;
|
||||
export class FilesController extends AdaptableController {
|
||||
|
||||
getFileData(config, filename) {
|
||||
return this.adapter.getFileData(filename);
|
||||
}
|
||||
|
||||
getHandler() {
|
||||
return (req, res) => {
|
||||
let config = new Config(req.params.appId);
|
||||
let filename = req.params.filename;
|
||||
this._filesAdapter.getFileData(config, filename).then((data) => {
|
||||
res.status(200);
|
||||
var contentType = mime.lookup(filename);
|
||||
res.set('Content-type', contentType);
|
||||
res.end(data);
|
||||
}).catch((error) => {
|
||||
res.status(404);
|
||||
res.set('Content-type', 'text/plain');
|
||||
res.end('File not found.');
|
||||
createFile(config, filename, data, contentType) {
|
||||
|
||||
let extname = path.extname(filename);
|
||||
|
||||
const hasExtension = extname.length > 0;
|
||||
|
||||
if (!hasExtension && contentType && mime.extension(contentType)) {
|
||||
filename = filename + '.' + mime.extension(contentType);
|
||||
} else if (hasExtension && !contentType) {
|
||||
contentType = mime.lookup(filename);
|
||||
}
|
||||
|
||||
filename = randomHexString(32) + '_' + filename;
|
||||
|
||||
var location = this.adapter.getFileLocation(config, filename);
|
||||
return this.adapter.createFile(filename, data, contentType).then(() => {
|
||||
return Promise.resolve({
|
||||
url: location,
|
||||
name: filename
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
createHandler() {
|
||||
return (req, res, next) => {
|
||||
if (!req.body || !req.body.length) {
|
||||
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR,
|
||||
'Invalid file upload.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.params.filename.length > 128) {
|
||||
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
|
||||
'Filename too long.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.params.filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) {
|
||||
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
|
||||
'Filename contains invalid characters.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// If a content-type is included, we'll add an extension so we can
|
||||
// return the same content-type.
|
||||
let extension = '';
|
||||
let hasExtension = req.params.filename.indexOf('.') > 0;
|
||||
let contentType = req.get('Content-type');
|
||||
if (!hasExtension && contentType && mime.extension(contentType)) {
|
||||
extension = '.' + mime.extension(contentType);
|
||||
}
|
||||
|
||||
let filename = randomHexString(32) + '_' + req.params.filename + extension;
|
||||
this._filesAdapter.createFile(req.config, filename, req.body).then(() => {
|
||||
res.status(201);
|
||||
var location = this._filesAdapter.getFileLocation(req.config, filename);
|
||||
res.set('Location', location);
|
||||
res.json({ url: location, name: filename });
|
||||
}).catch((error) => {
|
||||
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR,
|
||||
'Could not store file.'));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
deleteHandler() {
|
||||
return (req, res, next) => {
|
||||
this._filesAdapter.deleteFile(req.config, req.params.filename).then(() => {
|
||||
res.status(200);
|
||||
// TODO: return useful JSON here?
|
||||
res.end();
|
||||
}).catch((error) => {
|
||||
next(new Parse.Error(Parse.Error.FILE_DELETE_ERROR,
|
||||
'Could not delete file.'));
|
||||
});
|
||||
};
|
||||
deleteFile(config, filename) {
|
||||
return this.adapter.deleteFile(filename);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,36 +62,14 @@ export class FilesController {
|
||||
if (filename.indexOf('tfss-') === 0) {
|
||||
fileObject['url'] = 'http://files.parsetfss.com/' + config.fileKey + '/' + encodeURIComponent(filename);
|
||||
} else {
|
||||
fileObject['url'] = this._filesAdapter.getFileLocation(config, filename);
|
||||
fileObject['url'] = this.adapter.getFileLocation(config, filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getExpressRouter() {
|
||||
let router = express.Router();
|
||||
router.get('/files/:appId/:filename', this.getHandler());
|
||||
|
||||
router.post('/files', function(req, res, next) {
|
||||
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
|
||||
'Filename not provided.'));
|
||||
});
|
||||
|
||||
router.post('/files/:filename',
|
||||
Middlewares.allowCrossDomain,
|
||||
BodyParser.raw({type: '*/*', limit: '20mb'}),
|
||||
Middlewares.handleParseHeaders,
|
||||
this.createHandler()
|
||||
);
|
||||
|
||||
router.delete('/files/:filename',
|
||||
Middlewares.allowCrossDomain,
|
||||
Middlewares.handleParseHeaders,
|
||||
Middlewares.enforceMasterKeyAccess,
|
||||
this.deleteHandler()
|
||||
);
|
||||
|
||||
return router;
|
||||
expectedAdapterType() {
|
||||
return FilesAdapter;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
/** @flow weak */
|
||||
|
||||
import * as DatabaseAdapter from "../DatabaseAdapter";
|
||||
import * as triggers from "../triggers";
|
||||
import * as Parse from "parse/node";
|
||||
import * as request from "request";
|
||||
|
||||
const DefaultHooksCollectionName = "_Hooks";
|
||||
|
||||
export class HooksController {
|
||||
_applicationId:string;
|
||||
_collectionPrefix:string;
|
||||
_collection;
|
||||
|
||||
constructor(applicationId:string, collectionPrefix:string = '') {
|
||||
this._applicationId = applicationId;
|
||||
this._collectionPrefix = collectionPrefix;
|
||||
}
|
||||
|
||||
load() {
|
||||
return this._getHooks().then(hooks => {
|
||||
hooks = hooks || [];
|
||||
hooks.forEach((hook) => {
|
||||
this.addHookToTriggers(hook);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getCollection() {
|
||||
if (this._collection) {
|
||||
return Promise.resolve(this._collection)
|
||||
}
|
||||
|
||||
let database = DatabaseAdapter.getDatabaseConnection(this._applicationId, this._collectionPrefix);
|
||||
return database.adaptiveCollection(DefaultHooksCollectionName).then(collection => {
|
||||
this._collection = collection;
|
||||
return collection;
|
||||
});
|
||||
}
|
||||
|
||||
getFunction(functionName) {
|
||||
return this._getHooks({ functionName: functionName }, 1).then(results => results[0]);
|
||||
}
|
||||
|
||||
getFunctions() {
|
||||
return this._getHooks({ functionName: { $exists: true } });
|
||||
}
|
||||
|
||||
getTrigger(className, triggerName) {
|
||||
return this._getHooks({ className: className, triggerName: triggerName }, 1).then(results => results[0]);
|
||||
}
|
||||
|
||||
getTriggers() {
|
||||
return this._getHooks({ className: { $exists: true }, triggerName: { $exists: true } });
|
||||
}
|
||||
|
||||
deleteFunction(functionName) {
|
||||
triggers.removeFunction(functionName, this._applicationId);
|
||||
return this._removeHooks({ functionName: functionName });
|
||||
}
|
||||
|
||||
deleteTrigger(className, triggerName) {
|
||||
triggers.removeTrigger(triggerName, className, this._applicationId);
|
||||
return this._removeHooks({ className: className, triggerName: triggerName });
|
||||
}
|
||||
|
||||
_getHooks(query, limit) {
|
||||
let options = limit ? { limit: limit } : undefined;
|
||||
return this.getCollection().then(collection => collection.find(query, options));
|
||||
}
|
||||
|
||||
_removeHooks(query) {
|
||||
return this.getCollection().then(collection => {
|
||||
return collection.deleteMany(query);
|
||||
}).then(() => {
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
saveHook(hook) {
|
||||
var query;
|
||||
if (hook.functionName && hook.url) {
|
||||
query = { functionName: hook.functionName }
|
||||
} else if (hook.triggerName && hook.className && hook.url) {
|
||||
query = { className: hook.className, triggerName: hook.triggerName }
|
||||
} else {
|
||||
throw new Parse.Error(143, "invalid hook declaration");
|
||||
}
|
||||
return this.getCollection()
|
||||
.then(collection => collection.upsertOne(query, hook))
|
||||
.then(() => {
|
||||
return hook;
|
||||
});
|
||||
}
|
||||
|
||||
addHookToTriggers(hook) {
|
||||
var wrappedFunction = wrapToHTTPRequest(hook);
|
||||
wrappedFunction.url = hook.url;
|
||||
if (hook.className) {
|
||||
triggers.addTrigger(hook.triggerName, hook.className, wrappedFunction, this._applicationId)
|
||||
} else {
|
||||
triggers.addFunction(hook.functionName, wrappedFunction, null, this._applicationId);
|
||||
}
|
||||
}
|
||||
|
||||
addHook(hook) {
|
||||
this.addHookToTriggers(hook);
|
||||
return this.saveHook(hook);
|
||||
}
|
||||
|
||||
createOrUpdateHook(aHook) {
|
||||
var hook;
|
||||
if (aHook && aHook.functionName && aHook.url) {
|
||||
hook = {};
|
||||
hook.functionName = aHook.functionName;
|
||||
hook.url = aHook.url;
|
||||
} else if (aHook && aHook.className && aHook.url && aHook.triggerName && triggers.Types[aHook.triggerName]) {
|
||||
hook = {};
|
||||
hook.className = aHook.className;
|
||||
hook.url = aHook.url;
|
||||
hook.triggerName = aHook.triggerName;
|
||||
|
||||
} else {
|
||||
throw new Parse.Error(143, "invalid hook declaration");
|
||||
}
|
||||
|
||||
return this.addHook(hook);
|
||||
};
|
||||
|
||||
createHook(aHook) {
|
||||
if (aHook.functionName) {
|
||||
return this.getFunction(aHook.functionName).then((result) => {
|
||||
if (result) {
|
||||
throw new Parse.Error(143, `function name: ${aHook.functionName} already exits`);
|
||||
} else {
|
||||
return this.createOrUpdateHook(aHook);
|
||||
}
|
||||
});
|
||||
} else if (aHook.className && aHook.triggerName) {
|
||||
return this.getTrigger(aHook.className, aHook.triggerName).then((result) => {
|
||||
if (result) {
|
||||
throw new Parse.Error(143, `class ${aHook.className} already has trigger ${aHook.triggerName}`);
|
||||
}
|
||||
return this.createOrUpdateHook(aHook);
|
||||
});
|
||||
}
|
||||
|
||||
throw new Parse.Error(143, "invalid hook declaration");
|
||||
};
|
||||
|
||||
updateHook(aHook) {
|
||||
if (aHook.functionName) {
|
||||
return this.getFunction(aHook.functionName).then((result) => {
|
||||
if (result) {
|
||||
return this.createOrUpdateHook(aHook);
|
||||
}
|
||||
throw new Parse.Error(143, `no function named: ${aHook.functionName} is defined`);
|
||||
});
|
||||
} else if (aHook.className && aHook.triggerName) {
|
||||
return this.getTrigger(aHook.className, aHook.triggerName).then((result) => {
|
||||
if (result) {
|
||||
return this.createOrUpdateHook(aHook);
|
||||
}
|
||||
throw new Parse.Error(143, `class ${aHook.className} does not exist`);
|
||||
});
|
||||
}
|
||||
throw new Parse.Error(143, "invalid hook declaration");
|
||||
};
|
||||
}
|
||||
|
||||
function wrapToHTTPRequest(hook) {
|
||||
return (req, res) => {
|
||||
let jsonBody = {};
|
||||
for (var i in req) {
|
||||
jsonBody[i] = req[i];
|
||||
}
|
||||
if (req.object) {
|
||||
jsonBody.object = req.object.toJSON();
|
||||
jsonBody.object.className = req.object.className;
|
||||
}
|
||||
if (req.original) {
|
||||
jsonBody.original = req.original.toJSON();
|
||||
jsonBody.original.className = req.original.className;
|
||||
}
|
||||
let jsonRequest = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(jsonBody)
|
||||
};
|
||||
|
||||
request.post(hook.url, jsonRequest, function (err, httpResponse, body) {
|
||||
var result;
|
||||
if (body) {
|
||||
if (typeof body == "string") {
|
||||
try {
|
||||
body = JSON.parse(body);
|
||||
} catch (e) {
|
||||
err = { error: "Malformed response", code: -1 };
|
||||
}
|
||||
}
|
||||
if (!err) {
|
||||
result = body.success;
|
||||
err = body.error;
|
||||
}
|
||||
}
|
||||
if (err) {
|
||||
return res.error(err);
|
||||
} else {
|
||||
return res.success(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default HooksController;
|
||||
@@ -0,0 +1,51 @@
|
||||
import { ParseCloudCodePublisher } from '../LiveQuery/ParseCloudCodePublisher';
|
||||
|
||||
export class LiveQueryController {
|
||||
classNames: any;
|
||||
liveQueryPublisher: any;
|
||||
|
||||
constructor(config: any) {
|
||||
let classNames;
|
||||
// If config is empty, we just assume no classs needs to be registered as LiveQuery
|
||||
if (!config || !config.classNames) {
|
||||
this.classNames = new Set();
|
||||
} else if (config.classNames instanceof Array) {
|
||||
this.classNames = new Set(config.classNames);
|
||||
} else {
|
||||
throw 'liveQuery.classes should be an array of string'
|
||||
}
|
||||
this.liveQueryPublisher = new ParseCloudCodePublisher(config);
|
||||
}
|
||||
|
||||
onAfterSave(className: string, currentObject: any, originalObject: any) {
|
||||
if (!this.hasLiveQuery(className)) {
|
||||
return;
|
||||
}
|
||||
let req = this._makePublisherRequest(currentObject, originalObject);
|
||||
this.liveQueryPublisher.onCloudCodeAfterSave(req);
|
||||
}
|
||||
|
||||
onAfterDelete(className: string, currentObject: any, originalObject: any) {
|
||||
if (!this.hasLiveQuery(className)) {
|
||||
return;
|
||||
}
|
||||
let req = this._makePublisherRequest(currentObject, originalObject);
|
||||
this.liveQueryPublisher.onCloudCodeAfterDelete(req);
|
||||
}
|
||||
|
||||
hasLiveQuery(className: string): boolean {
|
||||
return this.classNames.has(className);
|
||||
}
|
||||
|
||||
_makePublisherRequest(currentObject: any, originalObject: any): any {
|
||||
let req = {
|
||||
object: currentObject
|
||||
};
|
||||
if (currentObject) {
|
||||
req.original = originalObject;
|
||||
}
|
||||
return req;
|
||||
}
|
||||
}
|
||||
|
||||
export default LiveQueryController;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user