2020-07-15 20:10:33 +02:00
'use strict' ;
const Config = require ( '../lib/Config' ) ;
const Definitions = require ( '../lib/Options/Definitions' ) ;
const request = require ( '../lib/request' ) ;
const rest = require ( '../lib/rest' ) ;
const auth = require ( '../lib/Auth' ) ;
const uuid = require ( 'uuid' ) ;
2022-01-02 13:25:53 -05:00
describe ( 'Idempotency' , ( ) => {
2020-07-15 20:10:33 +02:00
// Parameters
/ * * E n a b l e T T L e x p i r a t i o n s i m u l a t e d b y r e m o v i n g e n t r y i n s t e a d o f w a i t i n g f o r M o n g o D B T T L m o n i t o r w h i c h
runs only every 60 s , so it can take up to 119 s until entry removal - ain ' t nobody got time for that * /
const SIMULATE _TTL = true ;
2022-01-02 13:25:53 -05:00
const ttl = 2 ;
const maxTimeOut = 4000 ;
2020-07-15 20:10:33 +02:00
// Helpers
async function deleteRequestEntry ( reqId ) {
const config = Config . get ( Parse . applicationId ) ;
const res = await rest . find (
config ,
auth . master ( config ) ,
'_Idempotency' ,
{ reqId : reqId } ,
{ limit : 1 }
) ;
2020-10-25 15:06:58 -05:00
await rest . del ( config , auth . master ( config ) , '_Idempotency' , res . results [ 0 ] . objectId ) ;
2020-07-15 20:10:33 +02:00
}
async function setup ( options ) {
await reconfigureServer ( {
appId : Parse . applicationId ,
masterKey : Parse . masterKey ,
serverURL : Parse . serverURL ,
idempotencyOptions : options ,
} ) ;
}
// Setups
beforeEach ( async ( ) => {
2020-10-02 00:19:26 +02:00
if ( SIMULATE _TTL ) {
jasmine . DEFAULT _TIMEOUT _INTERVAL = 200000 ;
}
2020-07-15 20:10:33 +02:00
await setup ( {
2020-10-25 15:06:58 -05:00
paths : [ 'functions/.*' , 'jobs/.*' , 'classes/.*' , 'users' , 'installations' ] ,
2022-01-02 13:25:53 -05:00
ttl : ttl ,
2020-07-15 20:10:33 +02:00
} ) ;
} ) ;
2022-01-02 13:25:53 -05:00
2024-08-11 07:24:50 -05:00
afterEach ( ( ) => {
jasmine . DEFAULT _TIMEOUT _INTERVAL = process . env . PARSE _SERVER _TEST _TIMEOUT || 10000 ;
} ) ;
2020-07-15 20:10:33 +02:00
// Tests
2024-08-13 22:13:19 +02:00
it _id ( 'e25955fd-92eb-4b22-b8b7-38980e5cb223' ) ( it ) ( 'should enforce idempotency for cloud code function' , async ( ) => {
2020-07-15 20:10:33 +02:00
let counter = 0 ;
Parse . Cloud . define ( 'myFunction' , ( ) => {
counter ++ ;
} ) ;
const params = {
method : 'POST' ,
url : 'http://localhost:8378/1/functions/myFunction' ,
headers : {
'X-Parse-Application-Id' : Parse . applicationId ,
'X-Parse-Master-Key' : Parse . masterKey ,
2020-10-02 00:19:26 +02:00
'X-Parse-Request-Id' : 'abc-123' ,
} ,
2020-07-15 20:10:33 +02:00
} ;
2022-01-02 13:25:53 -05:00
expect ( Config . get ( Parse . applicationId ) . idempotencyOptions . ttl ) . toBe ( ttl ) ;
2020-07-15 20:10:33 +02:00
await request ( params ) ;
await request ( params ) . then ( fail , e => {
expect ( e . status ) . toEqual ( 400 ) ;
2020-10-02 00:19:26 +02:00
expect ( e . data . error ) . toEqual ( 'Duplicate request' ) ;
2020-07-15 20:10:33 +02:00
} ) ;
expect ( counter ) . toBe ( 1 ) ;
} ) ;
2024-08-13 22:13:19 +02:00
it _id ( 'be2fbe16-8178-485e-9a12-6fb541096480' ) ( it ) ( 'should delete request entry after TTL' , async ( ) => {
2020-07-15 20:10:33 +02:00
let counter = 0 ;
Parse . Cloud . define ( 'myFunction' , ( ) => {
counter ++ ;
} ) ;
const params = {
method : 'POST' ,
url : 'http://localhost:8378/1/functions/myFunction' ,
headers : {
'X-Parse-Application-Id' : Parse . applicationId ,
'X-Parse-Master-Key' : Parse . masterKey ,
2020-10-02 00:19:26 +02:00
'X-Parse-Request-Id' : 'abc-123' ,
} ,
2020-07-15 20:10:33 +02:00
} ;
await expectAsync ( request ( params ) ) . toBeResolved ( ) ;
if ( SIMULATE _TTL ) {
await deleteRequestEntry ( 'abc-123' ) ;
} else {
2022-01-02 13:25:53 -05:00
await new Promise ( resolve => setTimeout ( resolve , maxTimeOut ) ) ;
2020-07-15 20:10:33 +02:00
}
await expectAsync ( request ( params ) ) . toBeResolved ( ) ;
expect ( counter ) . toBe ( 2 ) ;
} ) ;
2022-03-18 15:16:09 +01:00
it _only _db ( 'postgres' ) (
'should delete request entry when postgress ttl function is called' ,
async ( ) => {
const client = Config . get ( Parse . applicationId ) . database . adapter . _client ;
let counter = 0 ;
Parse . Cloud . define ( 'myFunction' , ( ) => {
counter ++ ;
} ) ;
const params = {
method : 'POST' ,
url : 'http://localhost:8378/1/functions/myFunction' ,
headers : {
'X-Parse-Application-Id' : Parse . applicationId ,
'X-Parse-Master-Key' : Parse . masterKey ,
'X-Parse-Request-Id' : 'abc-123' ,
} ,
} ;
await expectAsync ( request ( params ) ) . toBeResolved ( ) ;
await expectAsync ( request ( params ) ) . toBeRejected ( ) ;
await new Promise ( resolve => setTimeout ( resolve , maxTimeOut ) ) ;
await client . one ( 'SELECT idempotency_delete_expired_records()' ) ;
await expectAsync ( request ( params ) ) . toBeResolved ( ) ;
expect ( counter ) . toBe ( 2 ) ;
}
) ;
2022-01-02 13:25:53 -05:00
2024-08-13 22:13:19 +02:00
it _id ( 'e976d0cc-a57f-45d4-9472-b9b052db6490' ) ( it ) ( 'should enforce idempotency for cloud code jobs' , async ( ) => {
2020-07-15 20:10:33 +02:00
let counter = 0 ;
Parse . Cloud . job ( 'myJob' , ( ) => {
counter ++ ;
} ) ;
const params = {
method : 'POST' ,
url : 'http://localhost:8378/1/jobs/myJob' ,
headers : {
'X-Parse-Application-Id' : Parse . applicationId ,
'X-Parse-Master-Key' : Parse . masterKey ,
2020-10-02 00:19:26 +02:00
'X-Parse-Request-Id' : 'abc-123' ,
} ,
2020-07-15 20:10:33 +02:00
} ;
await expectAsync ( request ( params ) ) . toBeResolved ( ) ;
await request ( params ) . then ( fail , e => {
expect ( e . status ) . toEqual ( 400 ) ;
2020-10-02 00:19:26 +02:00
expect ( e . data . error ) . toEqual ( 'Duplicate request' ) ;
2020-07-15 20:10:33 +02:00
} ) ;
expect ( counter ) . toBe ( 1 ) ;
} ) ;
2024-08-13 22:13:19 +02:00
it _id ( '7c84a3d4-e1b6-4a0d-99f1-af3cf1a6b3d8' ) ( it ) ( 'should enforce idempotency for class object creation' , async ( ) => {
2020-07-15 20:10:33 +02:00
let counter = 0 ;
Parse . Cloud . afterSave ( 'MyClass' , ( ) => {
counter ++ ;
} ) ;
const params = {
method : 'POST' ,
url : 'http://localhost:8378/1/classes/MyClass' ,
headers : {
'X-Parse-Application-Id' : Parse . applicationId ,
'X-Parse-Master-Key' : Parse . masterKey ,
2020-10-02 00:19:26 +02:00
'X-Parse-Request-Id' : 'abc-123' ,
} ,
2020-07-15 20:10:33 +02:00
} ;
await expectAsync ( request ( params ) ) . toBeResolved ( ) ;
await request ( params ) . then ( fail , e => {
expect ( e . status ) . toEqual ( 400 ) ;
2020-10-02 00:19:26 +02:00
expect ( e . data . error ) . toEqual ( 'Duplicate request' ) ;
2020-07-15 20:10:33 +02:00
} ) ;
expect ( counter ) . toBe ( 1 ) ;
} ) ;
2024-08-13 22:13:19 +02:00
it _id ( 'a030f2dd-5d21-46ac-b53d-9d714f35d72a' ) ( it ) ( 'should enforce idempotency for user object creation' , async ( ) => {
2020-07-15 20:10:33 +02:00
let counter = 0 ;
Parse . Cloud . afterSave ( '_User' , ( ) => {
counter ++ ;
} ) ;
const params = {
method : 'POST' ,
url : 'http://localhost:8378/1/users' ,
body : {
2020-10-02 00:19:26 +02:00
username : 'user' ,
password : 'pass' ,
2020-07-15 20:10:33 +02:00
} ,
headers : {
'X-Parse-Application-Id' : Parse . applicationId ,
'X-Parse-Master-Key' : Parse . masterKey ,
2020-10-02 00:19:26 +02:00
'X-Parse-Request-Id' : 'abc-123' ,
} ,
2020-07-15 20:10:33 +02:00
} ;
await expectAsync ( request ( params ) ) . toBeResolved ( ) ;
await request ( params ) . then ( fail , e => {
expect ( e . status ) . toEqual ( 400 ) ;
2020-10-02 00:19:26 +02:00
expect ( e . data . error ) . toEqual ( 'Duplicate request' ) ;
2020-07-15 20:10:33 +02:00
} ) ;
expect ( counter ) . toBe ( 1 ) ;
} ) ;
2024-08-13 22:13:19 +02:00
it _id ( '064c469b-091c-4ba9-9043-be461f26a3eb' ) ( it ) ( 'should enforce idempotency for installation object creation' , async ( ) => {
2020-07-15 20:10:33 +02:00
let counter = 0 ;
Parse . Cloud . afterSave ( '_Installation' , ( ) => {
counter ++ ;
} ) ;
const params = {
method : 'POST' ,
url : 'http://localhost:8378/1/installations' ,
body : {
2020-10-02 00:19:26 +02:00
installationId : '1' ,
deviceType : 'ios' ,
2020-07-15 20:10:33 +02:00
} ,
headers : {
'X-Parse-Application-Id' : Parse . applicationId ,
'X-Parse-Master-Key' : Parse . masterKey ,
2020-10-02 00:19:26 +02:00
'X-Parse-Request-Id' : 'abc-123' ,
} ,
2020-07-15 20:10:33 +02:00
} ;
await expectAsync ( request ( params ) ) . toBeResolved ( ) ;
await request ( params ) . then ( fail , e => {
expect ( e . status ) . toEqual ( 400 ) ;
2020-10-02 00:19:26 +02:00
expect ( e . data . error ) . toEqual ( 'Duplicate request' ) ;
2020-07-15 20:10:33 +02:00
} ) ;
expect ( counter ) . toBe ( 1 ) ;
} ) ;
2024-08-13 22:13:19 +02:00
it _id ( 'f11670b6-fa9c-4f21-a268-ae4b6bbff7fd' ) ( it ) ( 'should not interfere with calls of different request ID' , async ( ) => {
2020-07-15 20:10:33 +02:00
let counter = 0 ;
Parse . Cloud . afterSave ( 'MyClass' , ( ) => {
counter ++ ;
} ) ;
const promises = [ ... Array ( 100 ) . keys ( ) ] . map ( ( ) => {
const params = {
method : 'POST' ,
url : 'http://localhost:8378/1/classes/MyClass' ,
headers : {
'X-Parse-Application-Id' : Parse . applicationId ,
'X-Parse-Master-Key' : Parse . masterKey ,
2020-10-02 00:19:26 +02:00
'X-Parse-Request-Id' : uuid . v4 ( ) ,
} ,
2020-07-15 20:10:33 +02:00
} ;
return request ( params ) ;
} ) ;
await expectAsync ( Promise . all ( promises ) ) . toBeResolved ( ) ;
expect ( counter ) . toBe ( 100 ) ;
} ) ;
2024-08-13 22:13:19 +02:00
it _id ( '0ecd2cd2-dafb-4a2b-bb2b-9ad4c9aca777' ) ( it ) ( 'should re-throw any other error unchanged when writing request entry fails for any other reason' , async ( ) => {
2020-10-25 15:06:58 -05:00
spyOn ( rest , 'create' ) . and . rejectWith ( new Parse . Error ( 0 , 'some other error' ) ) ;
2020-07-15 20:10:33 +02:00
Parse . Cloud . define ( 'myFunction' , ( ) => { } ) ;
const params = {
method : 'POST' ,
url : 'http://localhost:8378/1/functions/myFunction' ,
headers : {
'X-Parse-Application-Id' : Parse . applicationId ,
'X-Parse-Master-Key' : Parse . masterKey ,
2020-10-02 00:19:26 +02:00
'X-Parse-Request-Id' : 'abc-123' ,
} ,
2020-07-15 20:10:33 +02:00
} ;
await request ( params ) . then ( fail , e => {
expect ( e . status ) . toEqual ( 400 ) ;
2020-10-02 00:19:26 +02:00
expect ( e . data . error ) . toEqual ( 'some other error' ) ;
2020-07-15 20:10:33 +02:00
} ) ;
} ) ;
it ( 'should use default configuration when none is set' , async ( ) => {
await setup ( { } ) ;
2020-10-02 00:19:26 +02:00
expect ( Config . get ( Parse . applicationId ) . idempotencyOptions . ttl ) . toBe (
Definitions . IdempotencyOptions . ttl . default
) ;
expect ( Config . get ( Parse . applicationId ) . idempotencyOptions . paths ) . toBe (
Definitions . IdempotencyOptions . paths . default
) ;
2020-07-15 20:10:33 +02:00
} ) ;
it ( 'should throw on invalid configuration' , async ( ) => {
await expectAsync ( setup ( { paths : 1 } ) ) . toBeRejected ( ) ;
await expectAsync ( setup ( { ttl : 'a' } ) ) . toBeRejected ( ) ;
await expectAsync ( setup ( { ttl : 0 } ) ) . toBeRejected ( ) ;
await expectAsync ( setup ( { ttl : - 1 } ) ) . toBeRejected ( ) ;
} ) ;
} ) ;