Skip to content
This repository has been archived by the owner on Apr 4, 2024. It is now read-only.

Commit

Permalink
feat: error structure changed, hip412validator class & tests adjusted
Browse files Browse the repository at this point in the history
Signed-off-by: michalrozekariane <[email protected]>
  • Loading branch information
michalrozekariane committed Feb 16, 2024
1 parent e880493 commit 51941cd
Show file tree
Hide file tree
Showing 10 changed files with 74 additions and 83 deletions.
5 changes: 1 addition & 4 deletions src/functions/createJsonMetadataFromCSV.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,7 @@ export const createJsonMetadataFromCSV = async ({

return {
isValid,
errors: {
general: errors.general,
missingAttributes: errors.missingAttributes,
},
errors,
savedJsonFilesLocation,
};
};
5 changes: 1 addition & 4 deletions src/types/jsonMetadataFromCSV.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
export interface JsonMetadataFromCSVInterface {
isValid: boolean;
errors: {
general: string[];
missingAttributes: string[];
};
errors: string[];
savedJsonFilesLocation: string;
}
11 changes: 4 additions & 7 deletions src/utils/constants/dictionary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,15 @@ export const dictionary = {
`Error in line number ${line}, column number ${column}. Check if your CSV file is well prepared.`,
invalidKeysDetected: (keys: string[]) => `Redundant key(s) detected: ['${keys.join("', '")}']`,
csvFileIsEmpty: (path: string) => `No metadata found in CSV file "${getFullSystemPath(path)}".`,
errorInRow: (fileName: string, line: number | string, error: string) =>
`Error at: line number ${line} in ${getFullSystemPath(fileName)} - ${error}`,
missingAttributesInRowWithFilePath: (filePath: string, row: number) =>
`In file: "${getFullSystemPath(filePath)}" in row ${row}`,
missingAttributes: 'There are missing attributes in the metadata object.',
arrayOfObjectsValidationError: (fileName: string, error: string) =>
`Error at: ${getFullSystemPath(fileName)} - ${error}`,
imageForNftNotFound:
'Image for NFT not found. The name of the image file should match its corresponding metadata file name (ex: 1.jpg with 1.json) or specify directly the "image" property.',
mediaFileNotSupported: 'Media file is not supported.',
unsupportedImageMimeType: 'Unsupported image MIME type.',
requiredFieldMissing: 'Required field is missing',
requiredTypeFieldIsMissing: 'The required "type" field is missing. ',
requiredAttributeFieldMissing: 'The required "attributes" field is missing. ',
requiredTypeFieldIsMissing: 'The required "type" field is missing.',
requiredAttributeFieldMissing: 'The required "attributes" field is missing.',
filePermissionDenied: 'Permission denied',
fileEmptyOrFormattingError: 'Unexpected end of JSON input',
directoryIsEmpty: 'Directory is empty',
Expand Down
10 changes: 7 additions & 3 deletions src/utils/helpers/validateObjectWithSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import keys from 'lodash/keys';
import { type z } from 'zod';
import type { ErrorMessageOptions } from 'zod-error';
import { generateErrorMessage } from 'zod-error';
import { ValidationError } from '../validationError';

export const noPropertiesErrorOptions: ErrorMessageOptions = {
prefix: '',
Expand Down Expand Up @@ -54,7 +55,7 @@ export const validationMetadataErrorOptions: ErrorMessageOptions = {
error: '',
},
message: { enabled: false },
transform: ({ errorMessage }) => `${errorMessage} `,
transform: ({ errorMessage }) => errorMessage,
};

export const validateObjectWithSchema = <T extends { [key: string]: string | unknown }>(
Expand All @@ -64,8 +65,11 @@ export const validateObjectWithSchema = <T extends { [key: string]: string | unk
): T => {
const validation = Schema.safeParse(object);
if (!validation.success) {
const errorMessage = generateErrorMessage(validation.error.issues, errorMessageOptions);
throw new Error(errorMessage);
const errorMessages = validation.error.issues.map((issue) =>
generateErrorMessage([issue], errorMessageOptions)
);

throw new ValidationError(errorMessages);
}

const parsedObjectWithSchema = Schema.parse(object);
Expand Down
60 changes: 23 additions & 37 deletions src/utils/services/Hip412Validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,19 @@ import {
} from '../validation-schemas/hip412Metadata.schema';
import {
validateObjectWithSchema,
noPropertiesErrorOptions,
validationMetadataErrorOptions,
} from '../helpers/validateObjectWithSchema';
import { errorToMessage } from '../helpers/errorToMessage';
import { MetadataObject } from '../../types/csv';
import { dictionary } from '../constants/dictionary';
import { getMetadataObjectsForValidation, getNFTsFromToken } from '../../api/mirrorNode';
import { nftMetadataDecoder } from '../helpers/nftMetadataDecoder';
import { ValidationError } from '../validationError';

interface FileValidationResult {
isValid: boolean;
fileName?: string;
errors: ValidationErrorsInterface;
}

interface ValidationErrorsInterface {
general: string[];
missingAttributes: string[];
errors: string[];
}

interface DirectoryValidationResult {
Expand All @@ -35,7 +30,6 @@ interface DirectoryValidationResult {
interface MetadataError {
fileName?: string;
general: string[];
missingAttributes: string[];
}

interface MetadataOnChainObjects {
Expand All @@ -46,52 +40,49 @@ interface MetadataOnChainObjects {

export class Hip412Validator {
static validateSingleMetadataObject(object: MetadataObject): FileValidationResult {
const errors: ValidationErrorsInterface = { general: [], missingAttributes: [] };
const errors: string[] = [];

try {
validateObjectWithSchema(Hip412MetadataSchema, object, validationMetadataErrorOptions);
} catch (error) {
errors.general.push(errorToMessage(error));
}

if (!object.attributes) {
errors.missingAttributes.push(dictionary.validation.missingAttributes);
} catch (err) {
if (err instanceof ValidationError) {
errors.push(...err.errors);
} else {
console.error(dictionary.errors.unhandledError);

Check warning on line 51 in src/utils/services/Hip412Validator.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and ubuntu-latest

Unexpected console statement
}
}

return {
isValid: errors.general.length === 0,
isValid: errors.length === 0,
errors,
};
}

static validateArrayOfObjects = (
metadataObjects: MetadataObject[],
filePath: string
filePath?: string
): FileValidationResult => {
const errors: ValidationErrorsInterface = { general: [], missingAttributes: [] };
const errors: string[] = [];

for (const [index, metadataObject] of metadataObjects.entries()) {
try {
validateObjectWithSchema(Hip412MetadataCSVSchema, metadataObject, noPropertiesErrorOptions);
validateObjectWithSchema(
Hip412MetadataCSVSchema,
metadataObject,
validationMetadataErrorOptions
);
} catch (e) {
errors.general.push(
dictionary.validation.errorInRow(
filePath,
index + 1,
errors.push(
dictionary.validation.arrayOfObjectsValidationError(
filePath || `object ${index + 1}`,
errorToMessage(
errorToMessage(e) === 'Required' ? dictionary.validation.requiredFieldMissing : e
)
)
);
}

if (!metadataObject.attributes) {
errors.missingAttributes.push(
dictionary.validation.missingAttributesInRowWithFilePath(filePath, index + 1)
);
}
}
return { isValid: errors.general.length === 0, errors };
return { isValid: errors.length === 0, errors };
};

static validateLocalFile(filePath: string): FileValidationResult {
Expand All @@ -102,10 +93,7 @@ export class Hip412Validator {
} catch (error) {
return {
isValid: false,
errors: {
general: [errorToMessage(error)],
missingAttributes: [],
},
errors: [errorToMessage(error)],
};
}
}
Expand All @@ -128,7 +116,6 @@ export class Hip412Validator {
errors: [
{
general: [dictionary.validation.directoryIsEmpty],
missingAttributes: [],
},
],
};
Expand All @@ -144,8 +131,7 @@ export class Hip412Validator {
allFilesValid = false;
errors.push({
fileName: file,
general: validationResult.errors.general,
missingAttributes: validationResult.errors.missingAttributes,
general: validationResult.errors,
});
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/utils/validationError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export class ValidationError extends Error {
errors: string[];

constructor(errors: string[]) {
super(errors.join(' '));
this.errors = errors;
this.name = 'ValidationError';
}
}
8 changes: 4 additions & 4 deletions test/integration/createJsonMetadataFromCsv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('createJsonMetadataFromCSV Integration Test', () => {
savedJsonFilesLocation: JSON_METADATA_INTEGRATION_TESTS_OUTPUT_FOLDER_PATH,
csvFilePath: CSV_EXAMPLE_WITH_ALL_FIELDS,
});
expect(result.errors.general).toHaveLength(0);
expect(result.errors).toHaveLength(0);
});

it(
Expand Down Expand Up @@ -87,7 +87,7 @@ describe('createJsonMetadataFromCSV Integration Test', () => {
csvFilePath: CSV_EXAMPLE_ONLY_REQUIRED_FIELDS,
});

expect(result.errors.general).toHaveLength(0);
expect(result.errors).toHaveLength(0);
});

it('createJsonMetadataFromCSV should complete without errors using CSV with only required fields and headers filled', async () => {
Expand All @@ -96,14 +96,14 @@ describe('createJsonMetadataFromCSV Integration Test', () => {
csvFilePath: CSV_EXAMPLE_ONLY_REQUIRED_FIELDS_AND_HEADERS,
});

expect(result.errors.general).toHaveLength(0);
expect(result.errors).toHaveLength(0);
});

it('createJsonMetadataFromCSV should return errors for missing required fields in CSV', async () => {
const result = await createJsonMetadataFromCSV({
savedJsonFilesLocation: JSON_METADATA_INTEGRATION_TESTS_OUTPUT_FOLDER_PATH,
csvFilePath: CSV_EXAMPLE_WITH_MISSING_REQUIRED_FIELDS,
});
expect(result.errors.general).toHaveLength(8);
expect(result.errors).toHaveLength(8);
});
});
6 changes: 2 additions & 4 deletions test/integration/validateLocalFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@ describe('Hip412Validator.validateLocalFile integration tests', () => {
it('should return an error if the file is empty', () => {
const validationResult = Hip412Validator.validateLocalFile(EMPTY_JSON_EXAMPLE_PATH);
expect(validationResult.isValid).toBe(false);
expect(validationResult.errors.general).toContain(
dictionary.validation.fileEmptyOrFormattingError
);
expect(validationResult.errors).toContain(dictionary.validation.fileEmptyOrFormattingError);
});

it('should validate correctly structured JSON file', () => {
const validationResult = Hip412Validator.validateLocalFile(CORRECT_EXAMPLE_PATH);
expect(validationResult.isValid).toBe(true);
expect(validationResult.errors.general.length).toBe(0);
expect(validationResult.errors.length).toBe(0);
});
});
12 changes: 4 additions & 8 deletions test/unit/validateLocalFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('Hip412Validator.validateLocalFile', () => {
});
const validationResult = Hip412Validator.validateLocalFile('mockPath.json');
expect(validationResult.isValid).toBe(false);
expect(validationResult.errors.general).toContain(dictionary.validation.filePermissionDenied);
expect(validationResult.errors).toContain(dictionary.validation.filePermissionDenied);
});

it('should handle JSON files with formatting errors', () => {
Expand All @@ -25,18 +25,14 @@ describe('Hip412Validator.validateLocalFile', () => {
});
const validationResult = Hip412Validator.validateLocalFile('mockPath.json');
expect(validationResult.isValid).toBe(false);
expect(validationResult.errors.general).toEqual([
dictionary.validation.fileEmptyOrFormattingError,
]);
expect(validationResult.errors).toEqual([dictionary.validation.fileEmptyOrFormattingError]);
});

it('should handle empty or non-existent JSON files', () => {
mockReadFileSync.mockReturnValue('');
const validationResult = Hip412Validator.validateLocalFile('path/to/empty.json');
expect(validationResult.isValid).toBe(false);
expect(validationResult.errors.general).toEqual([
dictionary.validation.fileEmptyOrFormattingError,
]);
expect(validationResult.errors).toEqual([dictionary.validation.fileEmptyOrFormattingError]);
});

it('should validate correctly structured JSON file', () => {
Expand All @@ -48,6 +44,6 @@ describe('Hip412Validator.validateLocalFile', () => {
mockReadFileSync.mockReturnValue(validJson);
const validationResult = Hip412Validator.validateLocalFile('path/to/valid.json');
expect(validationResult.isValid).toBe(true);
expect(validationResult.errors.general).toHaveLength(0);
expect(validationResult.errors).toHaveLength(0);
});
});
31 changes: 19 additions & 12 deletions test/unit/validateSingleMetadataObject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ const METADATA_OBJECT_WITH_ONLY_REQUIRED_FIELDS = {
type: 'image/jpeg',
};

const METADATA_OBJECT_WITH_REQUIRED_FIELDS_MISSING = {
const METADATA_OBJECT_WITH_ONE_REQUIRED_FIELD_MISSING = {
name: 'Example NFT 1',
type: 'image/jpeg',
};

const METADATA_OBJECT_WITH_TWO_REQUIRED_FIELDS_MISSING = {
name: 'Example NFT 1',
};

Expand Down Expand Up @@ -56,34 +61,36 @@ describe('Hip412Validator.validateSingleObject', () => {
it('should not return any errors for an object with all fields filled properly', () => {
const validationResult = validate(METADATA_OBECT_WITH_ALL_FIELDS);
expect(validationResult.isValid).toBe(true);
expect(validationResult.errors.general).toHaveLength(0);
expect(validationResult.errors).toHaveLength(0);
});

it('should not return any errors for an object with only required fields filled', () => {
const validationResult = validate(METADATA_OBJECT_WITH_ONLY_REQUIRED_FIELDS);
expect(validationResult.isValid).toBe(true);
expect(validationResult.errors.general).toHaveLength(0);
expect(validationResult.errors).toHaveLength(0);
});

it('should return one error for an object missing the image field', () => {
const validationResult = validate(METADATA_OBJECT_WITH_ONE_REQUIRED_FIELD_MISSING);
expect(validationResult.isValid).toBe(false);
expect(validationResult.errors).toHaveLength(1);
});

it('should return an error in errors.general for an object missing the image field', () => {
const validationResult = validate(METADATA_OBJECT_WITH_REQUIRED_FIELDS_MISSING);
it('should return two errors for an object missing the image and type field', () => {
const validationResult = validate(METADATA_OBJECT_WITH_TWO_REQUIRED_FIELDS_MISSING);
expect(validationResult.isValid).toBe(false);
expect(validationResult.errors.general).toHaveLength(1);
expect(validationResult.errors).toHaveLength(2);
});

it('should return an error for an object with an invalid image MIME type', () => {
const validationResult = validate(METADATA_OBJECT_WITH_INVALID_IMAGE_TYPE);
expect(validationResult.isValid).toBe(false);
expect(validationResult.errors.general).toEqual([
dictionary.validation.requiredTypeFieldIsMissing,
]);
expect(validationResult.errors).toEqual([dictionary.validation.requiredTypeFieldIsMissing]);
});

it('should return an error for an object with an invalid attributes structure', () => {
const validationResult = validate(METADATA_OBJECT_WITH_INVALID_ATTRIBUTES_STRUCTURE);
expect(validationResult.isValid).toBe(false);
expect(validationResult.errors.general).toEqual([
dictionary.validation.requiredAttributeFieldMissing,
]);
expect(validationResult.errors).toEqual([dictionary.validation.requiredAttributeFieldMissing]);
});
});

0 comments on commit 51941cd

Please sign in to comment.