text: 'use strict';
  
  const Config = require('../lib/Config');
  const SchemaController = require('../lib/Controllers/SchemaController');
  const dd = require('deep-diff');
  const TestUtils = require('../lib/TestUtils');
  
  let config;
  
  const hasAllPODobject = () => {
    const obj = new Parse.Object('HasAllPOD');
    obj.set('aNumber', 5);
    obj.set('aString', 'string');
    obj.set('aBool', true);
    obj.set('aDate', new Date());
    obj.set('aObject', { k1: 'value', k2: true, k3: 5 });
    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=' })
    );
    return obj;
  };
  
  describe('SchemaController', () => {
    beforeEach(() => {
      config = Config.get('test');
    });
  
    afterEach(async () => {
      await config.database.schemaCache.clear();
      await TestUtils.destroyAllDataPermanently(false);
    });
  
    it('can validate one object', done => {
      config.database
        .loadSchema()
        .then(schema => {
          return schema.validateObject('TestObject', { a: 1, b: 'yo', c: false });
        })
        .then(
          () => {
            done();
          },
          error => {
            jfail(error);
            done();
          }
        );
    });
  
    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(
          () => {
            done();
          },
          error => {
            jfail(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 });
        })
        .then(schema => {
          return schema.validateObject('Foo', { x: false, y: 'YY', z: 1 });
        })
        .then(() => {
          done();
        });
    });
  
    it('can validate Relation object', done => {
      config.database
        .loadSchema()
        .then(schema => {
          return schema.validateObject('Stuff', {
            aRelation: { __type: 'Relation', className: 'Stuff' },
          });
        })
        .then(schema => {
          return schema
            .validateObject('Stuff', {
              aRelation: { __type: 'Pointer', className: 'Stuff' },
            })
            .then(
              () => {
                done.fail('expected invalidity');
              },
              () => done()
            );
        }, done.fail);
    });
  
    it('rejects inconsistent types', done => {
      config.database
        .loadSchema()
        .then(schema => {
          return schema.validateObject('Stuff', { bacon: 7 });
        })
        .then(schema => {
          return schema.validateObject('Stuff', { bacon: 'z' });
        })
        .then(
          () => {
            fail('expected invalidity');
            done();
          },
          () => done()
        );
    });
  
    it('updates when new fields are added', done => {
      config.database
        .loadSchema()
        .then(schema => {
          return schema.validateObject('Stuff', { bacon: 7 });
        })
        .then(schema => {
          return schema.validateObject('Stuff', { sausage: 8 });
        })
        .then(schema => {
          return schema.validateObject('Stuff', { sausage: 'ate' });
        })
        .then(
          () => {
            fail('expected invalidity');
            done();
          },
          () => done()
        );
    });
  
    it('class-level permissions test find', done => {
      config.database
        .loadSchema()
        .then(schema => {
          // Just to create a valid class
          return schema.validateObject('Stuff', { foo: 'bar' });
        })
        .then(schema => {
          return schema.setPermissions('Stuff', {
            find: {},
          });
        })
        .then(() => {
          const query = new Parse.Query('Stuff');
          return query.find();
        })
        .then(
          () => {
            fail('Class permissions should have rejected this query.');
            done();
          },
          () => {
            done();
          }
        );
    });
  
    it('class-level permissions test user', done => {
      let user;
      createTestUser()
        .then(u => {
          user = u;
          return config.database.loadSchema();
        })
        .then(schema => {
          // Just to create a valid class
          return schema.validateObject('Stuff', { foo: 'bar' });
        })
        .then(schema => {
          const find = {};
          find[user.id] = true;
          return schema.setPermissions('Stuff', {
            find: find,
          });
        })
        .then(() => {
          const query = new Parse.Query('Stuff');
          return query.find();
        })
        .then(
          () => {
            done();
          },
          () => {
            fail('Class permissions should have allowed this query.');
            done();
          }
        );
    });
  
    it('class-level permissions test get', done => {
      let obj;
      createTestUser().then(user => {
        return (
          config.database
            .loadSchema()
            // Create a valid class
            .then(schema => schema.validateObject('Stuff', { foo: 'bar' }))
            .then(schema => {
              const find = {};
              const get = {};
              get[user.id] = true;
              return schema.setPermissions('Stuff', {
                create: { '*': true },
                find: find,
                get: get,
              });
            })
            .then(() => {
              obj = new Parse.Object('Stuff');
              obj.set('foo', 'bar');
              return obj.save();
            })
            .then(o => {
              obj = o;
              const query = new Parse.Query('Stuff');
              return query.find();
            })
            .then(
              () => {
                fail('Class permissions should have rejected this query.');
                done();
              },
              () => {
                const query = new Parse.Query('Stuff');
                return query.get(obj.id).then(
                  () => {
                    done();
                  },
                  () => {
                    fail('Class permissions should have allowed this get query');
                    done();
                  }
                );
              }
            )
        );
      });
    });
  
    it('class-level permissions test count', done => {
      let obj;
      return (
        config.database
          .loadSchema()
          // Create a valid class
          .then(schema => schema.validateObject('Stuff', { foo: 'bar' }))
          .then(schema => {
            const count = {};
            return schema.setPermissions('Stuff', {
              create: { '*': true },
              find: { '*': true },
              count: count,
            });
          })
          .then(() => {
            obj = new Parse.Object('Stuff');
            obj.set('foo', 'bar');
            return obj.save();
          })
          .then(o => {
            obj = o;
            const query = new Parse.Query('Stuff');
            return query.find();
          })
          .then(results => {
            expect(results.length).toBe(1);
            const query = new Parse.Query('Stuff');
            return query.count();
          })
          .then(
            () => {
              fail('Class permissions should have rejected this query.');
            },
            err => {
              expect(err.message).toEqual(
                'Permission denied for action count on class Stuff.'
              );
              done();
            }
          )
      );
    });
  
    it('can add classes without needing an object', done => {
      config.database
        .loadSchema()
        .then(schema =>
          schema.addClassIfNotExists('NewClass', {
            foo: { type: 'String' },
          })
        )
        .then(actualSchema => {
          const expectedSchema = {
            className: 'NewClass',
            fields: {
              objectId: { type: 'String' },
              updatedAt: { type: 'Date' },
              createdAt: { type: 'Date' },
              ACL: { type: 'ACL' },
              foo: { type: 'String' },
            },
            classLevelPermissions: {
              find: { '*': true },
              get: { '*': true },
              create: { '*': true },
              update: { '*': true },
              delete: { '*': true },
              addField: { '*': true },
              protectedFields: { '*': [] },
            },
          };
          expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
          done();
        })
        .catch(error => {
          fail('Error creating class: ' + JSON.stringify(error));
        });
    });
  
    it('can update classes without needing an object', done => {
      const levelPermissions = {
        find: { '*': true },
        get: { '*': true },
        create: { '*': true },
        update: { '*': true },
        delete: { '*': true },
        addField: { '*': true },
        protectedFields: { '*': [] },
      };
      config.database.loadSchema().then(schema => {
        schema
          .validateObject('NewClass', { foo: 2 })
          .then(() => schema.reloadData())
          .then(() =>
            schema.updateClass(
              'NewClass',
              {
                fooOne: { type: 'Number' },
                fooTwo: { type: 'Array' },
                fooThree: { type: 'Date' },
                fooFour: { type: 'Object' },
                fooFive: { type: 'Relation', targetClass: '_User' },
                fooSix: { type: 'String' },
                fooSeven: { type: 'Object' },
                fooEight: { type: 'String' },
                fooNine: { type: 'String' },
                fooTeen: { type: 'Number' },
                fooEleven: { type: 'String' },
                fooTwelve: { type: 'String' },
                fooThirteen: { type: 'String' },
                fooFourteen: { type: 'String' },
                fooFifteen: { type: 'String' },
                fooSixteen: { type: 'String' },
                fooEighteen: { type: 'String' },
                fooNineteen: { type: 'String' },
              },
              levelPermissions,
              {},
              config.database
            )
          )
          .then(actualSchema => {
            const expectedSchema = {
              className: 'NewClass',
              fields: {
                objectId: { type: 'String' },
                updatedAt: { type: 'Date' },
                createdAt: { type: 'Date' },
                ACL: { type: 'ACL' },
                foo: { type: 'Number' },
                fooOne: { type: 'Number' },
                fooTwo: { type: 'Array' },
                fooThree: { type: 'Date' },
                fooFour: { type: 'Object' },
                fooFive: { type: 'Relation', targetClass: '_User' },
                fooSix: { type: 'String' },
                fooSeven: { type: 'Object' },
                fooEight: { type: 'String' },
                fooNine: { type: 'String' },
                fooTeen: { type: 'Number' },
                fooEleven: { type: 'String' },
                fooTwelve: { type: 'String' },
                fooThirteen: { type: 'String' },
                fooFourteen: { type: 'String' },
                fooFifteen: { type: 'String' },
                fooSixteen: { type: 'String' },
                fooEighteen: { type: 'String' },
                fooNineteen: { type: 'String' },
              },
              classLevelPermissions: { ...levelPermissions },
              indexes: {
                _id_: { _id: 1 },
              },
            };
  
            expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
            done();
          })
          .catch(error => {
            console.trace(error);
            done();
            fail('Error creating class: ' + JSON.stringify(error));
          });
      });
    });
  
    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.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 => {
      // If two callers race to create the same schema, the response to the
      // race loser should be the same as if they hadn't been racing.
      config.database.loadSchema().then(schema => {
        const p1 = schema
          .addClassIfNotExists('NewClass', {
            foo: { type: 'String' },
          })
          .then(validateSchema)
          .catch(validateError);
        const p2 = schema
          .addClassIfNotExists('NewClass', {
            foo: { type: 'String' },
          })
          .then(validateSchema)
          .catch(validateError);
        let schemaValidated = false;
        function validateSchema(actualSchema) {
          const expectedSchema = {
            className: 'NewClass',
            fields: {
              objectId: { type: 'String' },
              updatedAt: { type: 'Date' },
              createdAt: { type: 'Date' },
              ACL: { type: 'ACL' },
              foo: { type: 'String' },
            },
            classLevelPermissions: {
              find: { '*': true },
              get: { '*': true },
              create: { '*': true },
              update: { '*': true },
              delete: { '*': true },
              addField: { '*': true },
              protectedFields: { '*': [] },
            },
          };
          expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
          schemaValidated = true;
        }
        let errorValidated = false;
        function validateError(error) {
          expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
          expect(error.message).toEqual('Class NewClass already exists.');
          errorValidated = true;
        }
        Promise.all([p1, p2]).then(() => {
          expect(schemaValidated).toEqual(true);
          expect(errorValidated).toEqual(true);
          done();
        });
      });
    });
  
    it('refuses to create classes with invalid names', done => {
      config.database.loadSchema().then(schema => {
        schema
          .addClassIfNotExists('_InvalidName', { foo: { type: 'String' } })
          .catch(error => {
            expect(error.error).toEqual(
              'Invalid classname: _InvalidName, classnames can only have alphanumeric characters and _, and must start with an alpha character '
            );
            done();
          });
      });
    });
  
    it('refuses to add fields with invalid names', done => {
      config.database
        .loadSchema()
        .then(schema =>
          schema.addClassIfNotExists('NewClass', {
            '0InvalidName': { type: 'String' },
          })
        )
        .catch(error => {
          expect(error.code).toEqual(Parse.Error.INVALID_KEY_NAME);
          expect(error.error).toEqual('invalid field name: 0InvalidName');
          done();
        });
    });
  
    it('refuses to explicitly create the default fields for custom classes', done => {
      config.database
        .loadSchema()
        .then(schema =>
          schema.addClassIfNotExists('NewClass', { objectId: { type: 'String' } })
        )
        .catch(error => {
          expect(error.code).toEqual(136);
          expect(error.error).toEqual('field objectId cannot be added');
          done();
        });
    });
  
    it('refuses to explicitly create the default fields for non-custom classes', done => {
      config.database
        .loadSchema()
        .then(schema =>
          schema.addClassIfNotExists('_Installation', {
            localeIdentifier: { type: 'String' },
          })
        )
        .catch(error => {
          expect(error.code).toEqual(136);
          expect(error.error).toEqual('field localeIdentifier cannot be added');
          done();
        });
    });
  
    it('refuses to add fields with invalid types', done => {
      config.database
        .loadSchema()
        .then(schema =>
          schema.addClassIfNotExists('NewClass', {
            foo: { type: 7 },
          })
        )
        .catch(error => {
          expect(error.code).toEqual(Parse.Error.INVALID_JSON);
          expect(error.error).toEqual('invalid JSON');
          done();
        });
    });
  
    it('refuses to add fields with invalid pointer types', done => {
      config.database
        .loadSchema()
        .then(schema =>
          schema.addClassIfNotExists('NewClass', {
            foo: { type: 'Pointer' },
          })
        )
        .catch(error => {
          expect(error.code).toEqual(135);
          expect(error.error).toEqual('type Pointer needs a class name');
          done();
        });
    });
  
    it('refuses to add fields with invalid pointer target', done => {
      config.database
        .loadSchema()
        .then(schema =>
          schema.addClassIfNotExists('NewClass', {
            foo: { type: 'Pointer', targetClass: 7 },
          })
        )
        .catch(error => {
          expect(error.code).toEqual(Parse.Error.INVALID_JSON);
          expect(error.error).toEqual('invalid JSON');
          done();
        });
    });
  
    it('refuses to add fields with invalid Relation type', done => {
      config.database
        .loadSchema()
        .then(schema =>
          schema.addClassIfNotExists('NewClass', {
            foo: { type: 'Relation', uselessKey: 7 },
          })
        )
        .catch(error => {
          expect(error.code).toEqual(135);
          expect(error.error).toEqual('type Relation needs a class name');
          done();
        });
    });
  
    it('refuses to add fields with invalid relation target', done => {
      config.database
        .loadSchema()
        .then(schema =>
          schema.addClassIfNotExists('NewClass', {
            foo: { type: 'Relation', targetClass: 7 },
          })
        )
        .catch(error => {
          expect(error.code).toEqual(Parse.Error.INVALID_JSON);
          expect(error.error).toEqual('invalid JSON');
          done();
        });
    });
  
    it('refuses to add fields with uncreatable pointer target class', done => {
      config.database
        .loadSchema()
        .then(schema =>
          schema.addClassIfNotExists('NewClass', {
            foo: { type: 'Pointer', targetClass: 'not a valid class name' },
          })
        )
        .catch(error => {
          expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
          expect(error.error).toEqual(
            'Invalid classname: not a valid class name, classnames can only have alphanumeric characters and _, and must start with an alpha character '
          );
          done();
        });
    });
  
    it('refuses to add fields with uncreatable relation target class', done => {
      config.database
        .loadSchema()
        .then(schema =>
          schema.addClassIfNotExists('NewClass', {
            foo: { type: 'Relation', targetClass: 'not a valid class name' },
          })
        )
        .catch(error => {
          expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
          expect(error.error).toEqual(
            'Invalid classname: not a valid class name, classnames can only have alphanumeric characters and _, and must start with an alpha character '
          );
          done();
        });
    });
  
    it('refuses to add fields with unknown types', done => {
      config.database
        .loadSchema()
        .then(schema =>
          schema.addClassIfNotExists('NewClass', {
            foo: { type: 'Unknown' },
          })
        )
        .catch(error => {
          expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE);
          expect(error.error).toEqual('invalid field type: Unknown');
          done();
        });
    });
  
    it('refuses to add CLP with incorrect find', done => {
      const levelPermissions = {
        find: { '*': false },
        get: { '*': true },
        create: { '*': true },
        update: { '*': true },
        delete: { '*': true },
        addField: { '*': true },
        protectedFields: { '*': ['email'] },
      };
      config.database.loadSchema().then(schema => {
        schema
          .validateObject('NewClass', {})
          .then(() => schema.reloadData())
          .then(() =>
            schema.updateClass(
              'NewClass',
              {},
              levelPermissions,
              {},
              config.database
            )
          )
          .then(done.fail)
          .catch(error => {
            expect(error.code).toEqual(Parse.Error.INVALID_JSON);
            done();
          });
      });
    });
  
    it('refuses to add CLP when incorrectly sending a string to protectedFields object value instead of an array', done => {
      const levelPermissions = {
        find: { '*': true },
        get: { '*': true },
        create: { '*': true },
        update: { '*': true },
        delete: { '*': true },
        addField: { '*': true },
        protectedFields: { '*': 'email' },
      };
      config.database.loadSchema().then(schema => {
        schema
          .validateObject('NewClass', {})
          .then(() => schema.reloadData())
          .then(() =>
            schema.updateClass(
              'NewClass',
              {},
              levelPermissions,
              {},
              config.database
            )
          )
          .then(done.fail)
          .catch(error => {
            expect(error.code).toEqual(Parse.Error.INVALID_JSON);
            done();
          });
      });
    });
  
    it('will create classes', done => {
      config.database
        .loadSchema()
        .then(schema =>
          schema.addClassIfNotExists('NewClass', {
            aNumber: { type: 'Number' },
            aString: { type: 'String' },
            aBool: { type: 'Boolean' },
            aDate: { type: 'Date' },
            aObject: { type: 'Object' },
            aArray: { type: 'Array' },
            aGeoPoint: { type: 'GeoPoint' },
            aFile: { type: 'File' },
            aPointer: {
              type: 'Pointer',
              targetClass: 'ThisClassDoesNotExistYet',
            },
            aRelation: { type: 'Relation', targetClass: 'NewClass' },
            aBytes: { type: 'Bytes' },
            aPolygon: { type: 'Polygon' },
          })
        )
        .then(actualSchema => {
          const expectedSchema = {
            className: 'NewClass',
            fields: {
              objectId: { type: 'String' },
              updatedAt: { type: 'Date' },
              createdAt: { type: 'Date' },
              ACL: { type: 'ACL' },
              aString: { type: 'String' },
              aNumber: { type: 'Number' },
              aBool: { type: 'Boolean' },
              aDate: { type: 'Date' },
              aObject: { type: 'Object' },
              aArray: { type: 'Array' },
              aGeoPoint: { type: 'GeoPoint' },
              aFile: { type: 'File' },
              aPointer: {
                type: 'Pointer',
                targetClass: 'ThisClassDoesNotExistYet',
              },
              aRelation: { type: 'Relation', targetClass: 'NewClass' },
              aBytes: { type: 'Bytes' },
              aPolygon: { type: 'Polygon' },
            },
            classLevelPermissions: {
              find: { '*': true },
              get: { '*': true },
              create: { '*': true },
              update: { '*': true },
              delete: { '*': true },
              addField: { '*': true },
              protectedFields: { '*': [] },
            },
          };
          expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
          done();
        });
    });
  
    it('creates the default fields for non-custom classes', done => {
      config.database
        .loadSchema()
        .then(schema =>
          schema.addClassIfNotExists('_Installation', {
            foo: { type: 'Number' },
          })
        )
        .then(actualSchema => {
          const expectedSchema = {
            className: '_Installation',
            fields: {
              objectId: { type: 'String' },
              updatedAt: { type: 'Date' },
              createdAt: { type: 'Date' },
              ACL: { type: 'ACL' },
              foo: { type: 'Number' },
              installationId: { type: 'String' },
              deviceToken: { type: 'String' },
              channels: { type: 'Array' },
              deviceType: { type: 'String' },
              pushType: { type: 'String' },
              GCMSenderId: { type: 'String' },
              timeZone: { type: 'String' },
              localeIdentifier: { type: 'String' },
              badge: { type: 'Number' },
              appVersion: { type: 'String' },
              appName: { type: 'String' },
              appIdentifier: { type: 'String' },
              parseVersion: { type: 'String' },
            },
            classLevelPermissions: {
              find: { '*': true },
              get: { '*': true },
              create: { '*': true },
              update: { '*': true },
              delete: { '*': true },
              addField: { '*': true },
              protectedFields: { '*': [] },
            },
          };
          expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
          done();
        });
    });
  
    it('creates non-custom classes which include relation field', done => {
      config.database
        .loadSchema()
        //as `_Role` is always created by default, we only get it here
        .then(schema => schema.getOneSchema('_Role'))
        .then(actualSchema => {
          const expectedSchema = {
            className: '_Role',
            fields: {
              objectId: { type: 'String' },
              updatedAt: { type: 'Date' },
              createdAt: { type: 'Date' },
              ACL: { type: 'ACL' },
              name: { type: 'String' },
              users: { type: 'Relation', targetClass: '_User' },
              roles: { type: 'Relation', targetClass: '_Role' },
            },
            classLevelPermissions: {
              find: { '*': true },
              get: { '*': true },
              create: { '*': true },
              update: { '*': true },
              delete: { '*': true },
              addField: { '*': true },
              protectedFields: { '*': [] },
            },
          };
          expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
          done();
        });
    });
  
    it('creates non-custom classes which include pointer field', done => {
      config.database
        .loadSchema()
        .then(schema => schema.addClassIfNotExists('_Session', {}))
        .then(actualSchema => {
          const expectedSchema = {
            className: '_Session',
            fields: {
              objectId: { type: 'String' },
              updatedAt: { type: 'Date' },
              createdAt: { type: 'Date' },
              restricted: { type: 'Boolean' },
              user: { type: 'Pointer', targetClass: '_User' },
              installationId: { type: 'String' },
              sessionToken: { type: 'String' },
              expiresAt: { type: 'Date' },
              createdWith: { type: 'Object' },
              ACL: { type: 'ACL' },
            },
            classLevelPermissions: {
              find: { '*': true },
              get: { '*': true },
              create: { '*': true },
              update: { '*': true },
              delete: { '*': true },
              addField: { '*': true },
              protectedFields: { '*': [] },
            },
          };
          expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
          done();
        });
    });
  
    it('refuses to create two geopoints', done => {
      config.database
        .loadSchema()
        .then(schema =>
          schema.addClassIfNotExists('NewClass', {
            geo1: { type: 'GeoPoint' },
            geo2: { type: 'GeoPoint' },
          })
        )
        .catch(error => {
          expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE);
          expect(error.error).toEqual(
            'currently, only one GeoPoint field may exist in an object. Adding geo2 when geo1 already exists.'
          );
          done();
        });
    });
  
    it('can check if a class exists', done => {
      config.database
        .loadSchema()
        .then(schema => {
          return schema
            .addClassIfNotExists('NewClass', {})
            .then(() => schema.reloadData({ clearCache: true }))
            .then(() => {
              schema
                .hasClass('NewClass')
                .then(hasClass => {
                  expect(hasClass).toEqual(true);
                  done();
                })
                .catch(fail);
  
              schema
                .hasClass('NonexistantClass')
                .then(hasClass => {
                  expect(hasClass).toEqual(false);
                  done();
                })
                .catch(fail);
            })
            .catch(error => {
              fail("Couldn't create class");
              jfail(error);
            });
        })
        .catch(() => fail("Couldn't load schema"));
    });
  
    it('refuses to delete fields from invalid class names', done => {
      config.database
        .loadSchema()
        .then(schema => schema.deleteField('fieldName', 'invalid class name'))
        .catch(error => {
          expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
          done();
        });
    });
  
    it('refuses to delete invalid fields', done => {
      config.database
        .loadSchema()
        .then(schema =>
          schema.deleteField('invalid field name', 'ValidClassName')
        )
        .catch(error => {
          expect(error.code).toEqual(Parse.Error.INVALID_KEY_NAME);
          done();
        });
    });
  
    it('refuses to delete the default fields', done => {
      config.database
        .loadSchema()
        .then(schema => schema.deleteField('installationId', '_Installation'))
        .catch(error => {
          expect(error.code).toEqual(136);
          expect(error.message).toEqual('field installationId cannot be changed');
          done();
        });
    });
  
    it('refuses to delete fields from nonexistant classes', done => {
      config.database
        .loadSchema()
        .then(schema => schema.deleteField('field', 'NoClass'))
        .catch(error => {
          expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
          expect(error.message).toEqual('Class NoClass does not exist.');
          done();
        });
    });
  
    it('refuses to delete fields that dont exist', done => {
      hasAllPODobject()
        .save()
        .then(() => config.database.loadSchema())
        .then(schema => schema.deleteField('missingField', 'HasAllPOD'))
        .catch(error => {
          expect(error.code).toEqual(255);
          expect(error.message).toEqual(
            'Field missingField does not exist, cannot delete.'
          );
          done();
        });
    });
  
    it('drops related collection when deleting relation field', done => {
      const obj1 = hasAllPODobject();
      obj1
        .save()
        .then(savedObj1 => {
          const obj2 = new Parse.Object('HasPointersAndRelations');
          obj2.set('aPointer', savedObj1);
          const 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 => {
            jfail(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(actualSchema => {
            const expectedSchema = {
              className: 'NewClass',
              fields: {
                objectId: { type: 'String' },
                updatedAt: { type: 'Date' },
                createdAt: { type: 'Date' },
                ACL: { type: 'ACL' },
                relationField: { type: 'Relation', targetClass: '_User' },
              },
              classLevelPermissions: {
                find: { '*': true },
                get: { '*': true },
                create: { '*': true },
                update: { '*': true },
                delete: { '*': true },
                addField: { '*': true },
                protectedFields: { '*': [] },
              },
            };
            expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
          })
          .then(() =>
            config.database.collectionExists('_Join:relationField:NewClass')
          )
          .then(exist => {
            on_db(
              'postgres',
              () => {
                // We create the table when creating the column
                expect(exist).toEqual(true);
              },
              () => {
                expect(exist).toEqual(false);
              }
            );
          })
          .then(() =>
            schema.deleteField('relationField', 'NewClass', config.database)
          )
          .then(() => schema.reloadData())
          .then(() => {
            const expectedSchema = {
              objectId: { type: 'String' },
              updatedAt: { type: 'Date' },
              createdAt: { type: 'Date' },
              ACL: { type: 'ACL' },
            };
            expect(dd(schema.schemaData.NewClass.fields, expectedSchema)).toEqual(
              undefined
            );
          })
          .then(done)
          .catch(done.fail);
      });
    });
  
    it('can delete string fields and resave as number field', done => {
      Parse.Object.disableSingleInstance();
      const obj1 = hasAllPODobject();
      const obj2 = hasAllPODobject();
      Parse.Object.saveAll([obj1, obj2])
        .then(() => config.database.loadSchema())
        .then(schema =>
          schema.deleteField('aString', 'HasAllPOD', config.database)
        )
        .then(() => new Parse.Query('HasAllPOD').get(obj1.id))
        .then(obj1Reloaded => {
          expect(obj1Reloaded.get('aString')).toEqual(undefined);
          obj1Reloaded.set('aString', ['not a string', 'this time']);
          obj1Reloaded
            .save()
            .then(obj1reloadedAgain => {
              expect(obj1reloadedAgain.get('aString')).toEqual([
                'not a string',
                'this time',
              ]);
              return new Parse.Query('HasAllPOD').get(obj2.id);
            })
            .then(obj2reloaded => {
              expect(obj2reloaded.get('aString')).toEqual(undefined);
              done();
            });
        })
        .catch(error => {
          jfail(error);
          done();
        });
    });
  
    it('can delete pointer fields and resave as string', done => {
      Parse.Object.disableSingleInstance();
      const obj1 = new Parse.Object('NewClass');
      obj1
        .save()
        .then(() => {
          obj1.set('aPointer', obj1);
          return obj1.save();
        })
        .then(obj1 => {
          expect(obj1.get('aPointer').id).toEqual(obj1.id);
        })
        .then(() => config.database.loadSchema())
        .then(schema =>
          schema.deleteField('aPointer', 'NewClass', config.database)
        )
        .then(() => new Parse.Query('NewClass').get(obj1.id))
        .then(obj1 => {
          expect(obj1.get('aPointer')).toEqual(undefined);
          obj1.set('aPointer', 'Now a string');
          return obj1.save();
        })
        .then(obj1 => {
          expect(obj1.get('aPointer')).toEqual('Now a string');
          done();
        });
    });
  
    it('can merge schemas', done => {
      expect(
        SchemaController.buildMergedSchemaObject(
          {
            _id: 'SomeClass',
            someType: { type: 'Number' },
          },
          {
            newType: { type: 'Number' },
          }
        )
      ).toEqual({
        someType: { type: 'Number' },
        newType: { type: 'Number' },
      });
      done();
    });
  
    it('can merge deletions', done => {
      expect(
        SchemaController.buildMergedSchemaObject(
          {
            _id: 'SomeClass',
            someType: { type: 'Number' },
            outDatedType: { type: 'String' },
          },
          {
            newType: { type: 'GeoPoint' },
            outDatedType: { __op: 'Delete' },
          }
        )
      ).toEqual({
        someType: { type: 'Number' },
        newType: { type: 'GeoPoint' },
      });
      done();
    });
  
    it('ignore default field when merge with system class', done => {
      expect(
        SchemaController.buildMergedSchemaObject(
          {
            _id: '_User',
            username: { type: 'String' },
            password: { type: 'String' },
            email: { type: 'String' },
            emailVerified: { type: 'Boolean' },
          },
          {
            emailVerified: { type: 'String' },
            customField: { type: 'String' },
          }
        )
      ).toEqual({
        customField: { type: 'String' },
      });
      done();
    });
  
    it('yields a proper schema mismatch error (#2661)', done => {
      const anObject = new Parse.Object('AnObject');
      const anotherObject = new Parse.Object('AnotherObject');
      const someObject = new Parse.Object('SomeObject');
      Parse.Object.saveAll([anObject, anotherObject, someObject])
        .then(() => {
          anObject.set('pointer', anotherObject);
          return anObject.save();
        })
        .then(() => {
          anObject.set('pointer', someObject);
          return anObject.save();
        })
        .then(
          () => {
            fail('shoud not save correctly');
            done();
          },
          err => {
            expect(err instanceof Parse.Error).toBeTruthy();
            expect(err.message).toEqual(
              'schema mismatch for AnObject.pointer; expected Pointer<AnotherObject> but got Pointer<SomeObject>'
            );
            done();
          }
        );
    });
  
    it('yields a proper schema mismatch error bis (#2661)', done => {
      const anObject = new Parse.Object('AnObject');
      const someObject = new Parse.Object('SomeObject');
      Parse.Object.saveAll([anObject, someObject])
        .then(() => {
          anObject.set('number', 1);
          return anObject.save();
        })
        .then(() => {
          anObject.set('number', someObject);
          return anObject.save();
        })
        .then(
          () => {
            fail('shoud not save correctly');
            done();
          },
          err => {
            expect(err instanceof Parse.Error).toBeTruthy();
            expect(err.message).toEqual(
              'schema mismatch for AnObject.number; expected Number but got Pointer<SomeObject>'
            );
            done();
          }
        );
    });
  
    it('yields a proper schema mismatch error ter (#2661)', done => {
      const anObject = new Parse.Object('AnObject');
      const someObject = new Parse.Object('SomeObject');
      Parse.Object.saveAll([anObject, someObject])
        .then(() => {
          anObject.set('pointer', someObject);
          return anObject.save();
        })
        .then(() => {
          anObject.set('pointer', 1);
          return anObject.save();
        })
        .then(
          () => {
            fail('shoud not save correctly');
            done();
          },
          err => {
            expect(err instanceof Parse.Error).toBeTruthy();
            expect(err.message).toEqual(
              'schema mismatch for AnObject.pointer; expected Pointer<SomeObject> but got Number'
            );
            done();
          }
        );
    });
  
    it('properly handles volatile _Schemas', done => {
      function validateSchemaStructure(schema) {
        expect(schema.hasOwnProperty('className')).toBe(true);
        expect(schema.hasOwnProperty('fields')).toBe(true);
        expect(schema.hasOwnProperty('classLevelPermissions')).toBe(true);
      }
      function validateSchemaDataStructure(schemaData) {
        Object.keys(schemaData).forEach(className => {
          const schema = schemaData[className];
          // Hooks has className...
          if (className != '_Hooks') {
            expect(schema.hasOwnProperty('className')).toBe(false);
          }
          expect(schema.hasOwnProperty('fields')).toBe(false);
          expect(schema.hasOwnProperty('classLevelPermissions')).toBe(false);
        });
      }
      let schema;
      config.database
        .loadSchema()
        .then(s => {
          schema = s;
          return schema.getOneSchema('_User', false);
        })
        .then(userSchema => {
          validateSchemaStructure(userSchema);
          validateSchemaDataStructure(schema.schemaData);
          return schema.getOneSchema('_PushStatus', true);
        })
        .then(pushStatusSchema => {
          validateSchemaStructure(pushStatusSchema);
          validateSchemaDataStructure(schema.schemaData);
        })
        .then(done)
        .catch(done.fail);
    });
  
    it('setAllClasses return classes if cache fails', async () => {
      const schema = await config.database.loadSchema();
  
      spyOn(schema._cache, 'setAllClasses').and.callFake(() =>
        Promise.reject('Oops!')
      );
      const errorSpy = spyOn(console, 'error').and.callFake(() => {});
      const allSchema = await schema.setAllClasses();
  
      expect(allSchema).toBeDefined();
      expect(errorSpy).toHaveBeenCalledWith(
        'Error saving schema to cache:',
        'Oops!'
      );
    });
  
    it('should not throw on null field types', async () => {
      const schema = await config.database.loadSchema();
      const result = await schema.enforceFieldExists(
        'NewClass',
        'fieldName',
        null
      );
      expect(result).toBeUndefined();
    });
  
    it('ensureFields should throw when schema is not set', async () => {
      const schema = await config.database.loadSchema();
      try {
        schema.ensureFields([
          {
            className: 'NewClass',
            fieldName: 'fieldName',
            type: 'String',
          },
        ]);
      } catch (e) {
        expect(e.message).toBe('Could not add field fieldName');
      }
    });
  });
  
  describe('Class Level Permissions for requiredAuth', () => {
    beforeEach(() => {
      config = Config.get('test');
    });
  
    function createUser() {
      const user = new Parse.User();
      user.set('username', 'hello');
      user.set('password', 'world');
      return user.signUp(null);
    }
  
    it('required auth test find', done => {
      config.database
        .loadSchema()
        .then(schema => {
          // Just to create a valid class
          return schema.validateObject('Stuff', { foo: 'bar' });
        })
        .then(schema => {
          return schema.setPermissions('Stuff', {
            find: {
              requiresAuthentication: true,
            },
          });
        })
        .then(() => {
          const query = new Parse.Query('Stuff');
          return query.find();
        })
        .then(
          () => {
            fail('Class permissions should have rejected this query.');
            done();
          },
          e => {
            expect(e.message).toEqual(
              'Permission denied, user needs to be authenticated.'
            );
            done();
          }
        );
    });
  
    it('required auth test find authenticated', done => {
      config.database
        .loadSchema()
        .then(schema => {
          // Just to create a valid class
          return schema.validateObject('Stuff', { foo: 'bar' });
        })
        .then(schema => {
          return schema.setPermissions('Stuff', {
            find: {
              requiresAuthentication: true,
            },
          });
        })
        .then(() => {
          return createUser();
        })
        .then(() => {
          const query = new Parse.Query('Stuff');
          return query.find();
        })
        .then(
          results => {
            expect(results.length).toEqual(0);
            done();
          },
          e => {
            console.error(e);
            fail('Should not have failed');
            done();
          }
        );
    });
  
    it('required auth should allow create authenticated', done => {
      config.database
        .loadSchema()
        .then(schema => {
          // Just to create a valid class
          return schema.validateObject('Stuff', { foo: 'bar' });
        })
        .then(schema => {
          return schema.setPermissions('Stuff', {
            create: {
              requiresAuthentication: true,
            },
          });
        })
        .then(() => {
          return createUser();
        })
        .then(() => {
          const stuff = new Parse.Object('Stuff');
          stuff.set('foo', 'bar');
          return stuff.save();
        })
        .then(
          () => {
            done();
          },
          e => {
            console.error(e);
            fail('Should not have failed');
            done();
          }
        );
    });
  
    it('required auth should reject create when not authenticated', done => {
      config.database
        .loadSchema()
        .then(schema => {
          // Just to create a valid class
          return schema.validateObject('Stuff', { foo: 'bar' });
        })
        .then(schema => {
          return schema.setPermissions('Stuff', {
            create: {
              requiresAuthentication: true,
            },
          });
        })
        .then(() => {
          const stuff = new Parse.Object('Stuff');
          stuff.set('foo', 'bar');
          return stuff.save();
        })
        .then(
          () => {
            fail('Class permissions should have rejected this query.');
            done();
          },
          e => {
            expect(e.message).toEqual(
              'Permission denied, user needs to be authenticated.'
            );
            done();
          }
        );
    });
  
    it('required auth test create/get/update/delete authenticated', done => {
      config.database
        .loadSchema()
        .then(schema => {
          // Just to create a valid class
          return schema.validateObject('Stuff', { foo: 'bar' });
        })
        .then(schema => {
          return schema.setPermissions('Stuff', {
            create: {
              requiresAuthentication: true,
            },
            get: {
              requiresAuthentication: true,
            },
            delete: {
              requiresAuthentication: true,
            },
            update: {
              requiresAuthentication: true,
            },
          });
        })
        .then(() => {
          return createUser();
        })
        .then(() => {
          const stuff = new Parse.Object('Stuff');
          stuff.set('foo', 'bar');
          return stuff.save().then(() => {
            const query = new Parse.Query('Stuff');
            return query.get(stuff.id);
          });
        })
        .then(gotStuff => {
          return gotStuff.save({ foo: 'baz' }).then(() => {
            return gotStuff.destroy();
          });
        })
        .then(
          () => {
            done();
          },
          e => {
            console.error(e);
            fail('Should not have failed');
            done();
          }
        );
    });
  
    it('required auth test create/get/update/delete not authenitcated', done => {
      config.database
        .loadSchema()
        .then(schema => {
          // Just to create a valid class
          return schema.validateObject('Stuff', { foo: 'bar' });
        })
        .then(schema => {
          return schema.setPermissions('Stuff', {
            get: {
              requiresAuthentication: true,
            },
            delete: {
              requiresAuthentication: true,
            },
            update: {
              requiresAuthentication: true,
            },
            create: {
              '*': true,
            },
          });
        })
        .then(() => {
          const stuff = new Parse.Object('Stuff');
          stuff.set('foo', 'bar');
          return stuff.save().then(() => {
            const query = new Parse.Query('Stuff');
            return query.get(stuff.id);
          });
        })
        .then(
          () => {
            fail('Should not succeed!');
            done();
          },
          e => {
            expect(e.message).toEqual(
              'Permission denied, user needs to be authenticated.'
            );
            done();
          }
        );
    });
  
    it('required auth test create/get/update/delete not authenitcated', done => {
      config.database
        .loadSchema()
        .then(schema => {
          // Just to create a valid class
          return schema.validateObject('Stuff', { foo: 'bar' });
        })
        .then(schema => {
          return schema.setPermissions('Stuff', {
            find: {
              requiresAuthentication: true,
            },
            delete: {
              requiresAuthentication: true,
            },
            update: {
              requiresAuthentication: true,
            },
            create: {
              '*': true,
            },
            get: {
              '*': true,
            },
          });
        })
        .then(() => {
          const stuff = new Parse.Object('Stuff');
          stuff.set('foo', 'bar');
          return stuff.save().then(() => {
            const query = new Parse.Query('Stuff');
            return query.get(stuff.id);
          });
        })
        .then(result => {
          expect(result.get('foo')).toEqual('bar');
          const query = new Parse.Query('Stuff');
          return query.find();
        })
        .then(
          () => {
            fail('Should not succeed!');
            done();
          },
          e => {
            expect(e.message).toEqual(
              'Permission denied, user needs to be authenticated.'
            );
            done();
          }
        );
    });
  
    it('required auth test create/get/update/delete with roles (#3753)', done => {
      let user;
      config.database
        .loadSchema()
        .then(schema => {
          // Just to create a valid class
          return schema.validateObject('Stuff', { foo: 'bar' });
        })
        .then(schema => {
          return schema.setPermissions('Stuff', {
            find: {
              requiresAuthentication: true,
              'role:admin': true,
            },
            create: { 'role:admin': true },
            update: { 'role:admin': true },
            delete: { 'role:admin': true },
            get: {
              requiresAuthentication: true,
              'role:admin': true,
            },
          });
        })
        .then(() => {
          const stuff = new Parse.Object('Stuff');
          stuff.set('foo', 'bar');
          return stuff
            .save(null, { useMasterKey: true })
            .then(() => {
              const query = new Parse.Query('Stuff');
              return query
                .get(stuff.id)
                .then(
                  () => {
                    done.fail('should not succeed');
                  },
                  () => {
                    return new Parse.Query('Stuff').find();
                  }
                )
                .then(
                  () => {
                    done.fail('should not succeed');
                  },
                  () => {
                    return Promise.resolve();
                  }
                );
            })
            .then(() => {
              return Parse.User.signUp('user', 'password').then(signedUpUser => {
                user = signedUpUser;
                const query = new Parse.Query('Stuff');
                return query.get(stuff.id, {
                  sessionToken: user.getSessionToken(),
                });
              });
            });
        })
        .then(result => {
          expect(result.get('foo')).toEqual('bar');
          const query = new Parse.Query('Stuff');
          return query.find({ sessionToken: user.getSessionToken() });
        })
        .then(
          results => {
            expect(results.length).toBe(1);
            done();
          },
          e => {
            console.error(e);
            done.fail(e);
          }
        );
    });
  });
  