diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js index 570b3eb00..47ffa1332 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js @@ -49,7 +49,7 @@ export const loadWorkspacesFromIdb = () => (dispatch) => { export const addWorkspace = (workspaceName) => (dispatch) => { const newWorkspace = { - uid: uuid() + "junk", + uid: uuid(), name: workspaceName }; diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js new file mode 100644 index 000000000..962628c4b --- /dev/null +++ b/packages/bruno-schema/src/collections/index.js @@ -0,0 +1,64 @@ +const Yup = require('yup'); +const { uidSchema } = require("../common"); + +const keyValueSchema = Yup.object({ + uid: uidSchema, + name: Yup.string().nullable().max(256, 'name must be 256 characters or less').defined(), + value: Yup.string().nullable().max(2048, 'value must be 2048 characters or less').defined(), + description: Yup.string().nullable().max(2048, 'description must be 2048 characters or less').defined(), + enabled: Yup.boolean().defined() +}).noUnknown(true).strict(); + +const requestTypeSchema = Yup.string().oneOf(['http', 'graphql']).required('type is required'); +const requestUrlSchema = Yup.string().min(0).max(2048, 'name must be 2048 characters or less').defined(); +const requestMethodSchema = Yup.string().oneOf(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']).required('method is required'); + +const requestBodySchema = Yup.object({ + mode: Yup.string().oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm']).required('mode is required'), + json: Yup.string().max(10240, 'json must be 10240 characters or less'), + text: Yup.string().max(10240, 'text must be 10240 characters or less'), + xml: Yup.string().max(10240, 'xml must be 10240 characters or less'), + formUrlEncoded: keyValueSchema, + multipartForm: keyValueSchema, +}).noUnknown(true).strict(); + +// Right now, the request schema is very tightly coupled with http request +// As we introduce more request types in the future, we will improve the definition to support +// schema structure based on other request type +const requestSchema = Yup.object({ + type: requestTypeSchema, + url: requestUrlSchema, + method: requestMethodSchema, + headers: Yup.array().of(keyValueSchema).required('headers are required'), + params: Yup.array().of(keyValueSchema).required('params are required'), + body: requestBodySchema +}).noUnknown(true).strict(); + +const itemSchema = Yup.object({ + uid: uidSchema, + type: Yup.string().oneOf(['request', 'folder']).required('type is required'), + name: Yup.string() + .min(1, 'name must be atleast 1 characters') + .max(50, 'name must be 100 characters or less') + .required('name is required'), + request: requestSchema.when('type', { + is: 'request', + then: (schema) => schema.required('request is required when item-type is request') + }), + items: Yup.lazy(() => Yup.array().of(itemSchema)) +}).noUnknown(true).strict(); + +const collectionSchema = Yup.object({ + uid: uidSchema, + name: Yup.string() + .min(1, 'name must be atleast 1 characters') + .max(50, 'name must be 100 characters or less') + .required('name is required'), + items: Yup.array().of(itemSchema) +}).noUnknown(true).strict(); + +module.exports = { + requestSchema, + itemSchema, + collectionSchema +}; \ No newline at end of file diff --git a/packages/bruno-schema/src/collections/index.spec.js b/packages/bruno-schema/src/collections/index.spec.js new file mode 100644 index 000000000..eacf06fe2 --- /dev/null +++ b/packages/bruno-schema/src/collections/index.spec.js @@ -0,0 +1,120 @@ +const { expect } = require('@jest/globals'); +const { uuid } = require("../utils/testUtils"); +const { collectionSchema } = require("./index"); + +describe('Collection Schema Validation', () => { + it('collection schema must validate successfully - simple collection, no items', async () => { + const collection = { + uid: uuid(), + name: 'My Collection' + }; + + const isValid = await collectionSchema.validate(collection); + expect(isValid).toBeTruthy(); + }); + + it('collection schema must validate successfully - simple collection, empty items', async () => { + const collection = { + uid: uuid(), + name: 'My Collection', + items: [] + }; + + const isValid = await collectionSchema.validate(collection); + expect(isValid).toBeTruthy(); + }); + + it('collection schema must validate successfully - simple collection, just a folder item', async () => { + const collection = { + uid: uuid(), + name: 'My Collection', + items: [{ + uid: uuid(), + name: 'A Folder', + type: 'folder' + }] + }; + + const isValid = await collectionSchema.validate(collection); + expect(isValid).toBeTruthy(); + }); + + it('collection schema must validate successfully - simple collection, just a request item', async () => { + const collection = { + uid: uuid(), + name: 'My Collection', + items: [{ + uid: uuid(), + name: 'Get Countries', + type: 'request', + request: { + type: 'http', + url: 'https://restcountries.com/v2/alpha/in', + method: 'GET', + headers: [], + params: [], + body: { + mode: 'none' + } + } + }] + }; + + const isValid = await collectionSchema.validate(collection); + expect(isValid).toBeTruthy(); + }); + + it('collection schema must validate successfully - simple collection, folder inside folder', async () => { + const collection = { + uid: uuid(), + name: 'My Collection', + items: [{ + uid: uuid(), + name: 'First Level Folder', + type: 'folder', + items: [{ + uid: uuid(), + name: 'Second Level Folder', + type: 'folder' + }] + }] + }; + + const isValid = await collectionSchema.validate(collection); + expect(isValid).toBeTruthy(); + }); + + it('collection schema must validate successfully - simple collection, [folder] [request + folder]', async () => { + const collection = { + uid: uuid(), + name: 'My Collection', + items: [{ + uid: uuid(), + name: 'First Level Folder', + type: 'folder', + items: [{ + uid: uuid(), + name: 'Get Countries', + type: 'request', + request: { + type: 'http', + url: 'https://restcountries.com/v2/alpha/in', + method: 'GET', + headers: [], + params: [], + body: { + mode: 'none' + } + } + }, { + uid: uuid(), + name: 'Second Level Folder', + type: 'folder' + }] + }] + }; + + const isValid = await collectionSchema.validate(collection); + expect(isValid).toBeTruthy(); + }); +}); diff --git a/packages/bruno-schema/src/collections/itemSchema.spec.js b/packages/bruno-schema/src/collections/itemSchema.spec.js new file mode 100644 index 000000000..ea41b314d --- /dev/null +++ b/packages/bruno-schema/src/collections/itemSchema.spec.js @@ -0,0 +1,57 @@ +const { expect } = require('@jest/globals'); +const { uuid, validationErrorWithMessages } = require("../utils/testUtils"); +const { itemSchema } = require("./index"); + +describe('Item Schema Validation', () => { + it('item schema must validate successfully - simple items', async () => { + const item = { + uid: uuid(), + name: 'A Folder', + type: 'folder' + }; + + const isValid = await itemSchema.validate(item); + expect(isValid).toBeTruthy(); + }); + + it('item schema must throw an error if name is missing', async () => { + const item = { + uid: uuid(), + type: 'folder' + }; + + return Promise.all([ + expect(itemSchema.validate(item)).rejects.toEqual( + validationErrorWithMessages('name is required') + ) + ]); + }); + + it('item schema must throw an error if name is empty', async () => { + const item = { + uid: uuid(), + name: '', + type: 'folder' + }; + + return Promise.all([ + expect(itemSchema.validate(item)).rejects.toEqual( + validationErrorWithMessages('name must be atleast 1 characters') + ) + ]); + }); + + it('item schema must throw an error if request is not present when item-type is request', async () => { + const item = { + uid: uuid(), + name: 'Get Users', + type: 'request' + }; + + return Promise.all([ + expect(itemSchema.validate(item)).rejects.toEqual( + validationErrorWithMessages('request is required when item-type is request') + ) + ]); + }); +}); \ No newline at end of file diff --git a/packages/bruno-schema/src/collections/requestSchema.spec.js b/packages/bruno-schema/src/collections/requestSchema.spec.js new file mode 100644 index 000000000..e15a91fa7 --- /dev/null +++ b/packages/bruno-schema/src/collections/requestSchema.spec.js @@ -0,0 +1,107 @@ +const { expect } = require('@jest/globals'); +const { uuid, validationErrorWithMessages } = require("../utils/testUtils"); +const { requestSchema } = require("./index"); + +describe('Request Schema Validation', () => { + it('request schema must validate successfully - simple request', async () => { + const request = { + type: 'http', + url: 'https://restcountries.com/v2/alpha/in', + method: 'GET', + headers: [], + params: [], + body: { + mode: 'none' + } + }; + + const isValid = await requestSchema.validate(request); + expect(isValid).toBeTruthy(); + }); + + it('request schema must throw an error of type is invalid', async () => { + const request = { + type: 'http-junk', + url: 'https://restcountries.com/v2/alpha/in', + method: 'GET', + headers: [], + params: [], + body: { + mode: 'none' + } + }; + + return Promise.all([ + expect(requestSchema.validate(request)).rejects.toEqual( + validationErrorWithMessages('type must be one of the following values: http, graphql') + ) + ]); + }); + + it('request schema must throw an error of method is invalid', async () => { + const request = { + type: 'http', + url: 'https://restcountries.com/v2/alpha/in', + method: 'GET-junk', + headers: [], + params: [], + body: { + mode: 'none' + } + }; + + return Promise.all([ + expect(requestSchema.validate(request)).rejects.toEqual( + validationErrorWithMessages('method must be one of the following values: GET, POST, PUT, DELETE, PATCH, HEAD') + ) + ]); + }); + + it('request schema must throw an error of header name is missing', async () => { + const request = { + type: 'http', + url: 'https://restcountries.com/v2/alpha/in', + method: 'GET', + headers: [{ + uid: uuid(), + value: 'Check', + description: '', + enabled: true + }], + params: [], + body: { + mode: 'none' + } + }; + + return Promise.all([ + expect(requestSchema.validate(request)).rejects.toEqual( + validationErrorWithMessages('headers[0].name must be defined') + ) + ]); + }); + + it('request schema must throw an error of param value is missing', async () => { + const request = { + type: 'http', + url: 'https://restcountries.com/v2/alpha/in', + method: 'GET', + headers: [], + params: [{ + uid: uuid(), + name: 'customerId', + description: '', + enabled: true + }], + body: { + mode: 'none' + } + }; + + return Promise.all([ + expect(requestSchema.validate(request)).rejects.toEqual( + validationErrorWithMessages('params[0].value must be defined') + ) + ]); + }); +}); \ No newline at end of file diff --git a/packages/bruno-schema/src/common/index.js b/packages/bruno-schema/src/common/index.js index 593288368..7e2bac1d1 100644 --- a/packages/bruno-schema/src/common/index.js +++ b/packages/bruno-schema/src/common/index.js @@ -3,6 +3,7 @@ const Yup = require('yup'); const uidSchema = Yup.string() .length(21, 'uid must be 21 characters in length') .matches(/^[a-zA-Z0-9]*$/, 'uid must be alphanumeric') + .required('uid is required') .strict(); module.exports = {