diff --git a/.eslintrc.json b/.eslintrc.json index 324e291c..9cbbd586 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,6 +3,7 @@ "browser": true, "es2020": true, "node": true, + "jquery": true, "jest": true }, "parser": "@typescript-eslint/parser", @@ -14,6 +15,7 @@ "sourceType": "module" }, "extends": [ + "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", "eslint:recommended", "plugin:prettier/recommended", @@ -39,7 +41,8 @@ "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-var-requires": "off", "@typescript-eslint/no-empty-interface": "off", - "@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }] + "@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }], + "@typescript-eslint/no-namespace": ["error", { "allowDeclarations": true }] }, "globals": { "React": "writable" diff --git a/cypress/e2e/api.cy.ts b/cypress/e2e/api.cy.ts new file mode 100644 index 00000000..e69b5dff --- /dev/null +++ b/cypress/e2e/api.cy.ts @@ -0,0 +1,29 @@ +describe('Website tests', () => { + Cypress.session.clearAllSavedSessions(); + + beforeEach(() => { + cy.login(Cypress.env('umami_user'), Cypress.env('umami_password')); + }); + + //let userId; + + it('creates a user.', () => { + cy.fixture('users').then(data => { + const userPost = data.userPost; + cy.request({ + method: 'POST', + url: '/api/users', + headers: { + 'Content-Type': 'application/json', + Authorization: Cypress.env('authorization'), + }, + body: userPost, + }).then(response => { + //userId = response.body.id; + expect(response.status).to.eq(200); + expect(response.body).to.have.property('username', 'cypress1'); + expect(response.body).to.have.property('role', 'User'); + }); + }); + }); +}); diff --git a/cypress/e2e/login.cy.ts b/cypress/e2e/login.cy.ts index 5831c81d..507b1b58 100644 --- a/cypress/e2e/login.cy.ts +++ b/cypress/e2e/login.cy.ts @@ -1,22 +1,36 @@ describe('Login tests', () => { + beforeEach(() => { + cy.visit('/login'); + }); + it( 'logs user in with correct credentials and logs user out', { defaultCommandTimeout: 10000, }, () => { - cy.visit('/login'); - cy.getDataTest('input-username').find('input').click(); - cy.getDataTest('input-username').find('input').type(Cypress.env('umami_user'), { delay: 50 }); - cy.getDataTest('input-password').find('input').click(); + cy.getDataTest('input-username').find('input').as('inputUsername').click(); + cy.get('@inputUsername').type(Cypress.env('umami_user'), { delay: 0 }); + cy.get('@inputUsername').click(); cy.getDataTest('input-password') .find('input') - .type(Cypress.env('umami_password'), { delay: 50 }); + .type(Cypress.env('umami_password'), { delay: 0 }); cy.getDataTest('button-submit').click(); cy.url().should('eq', Cypress.config().baseUrl + '/dashboard'); - cy.getDataTest('button-profile').click(); - cy.getDataTest('item-logout').click(); - cy.url().should('eq', Cypress.config().baseUrl + '/login'); + cy.logout(); }, ); + + it('login with blank inputs or incorrect credentials', () => { + cy.getDataTest('button-submit').click(); + cy.contains(/Required/i).should('be.visible'); + + cy.getDataTest('input-username').find('input').as('inputUsername'); + cy.get('@inputUsername').click(); + cy.get('@inputUsername').type(Cypress.env('umami_user'), { delay: 0 }); + cy.get('@inputUsername').click(); + cy.getDataTest('input-password').find('input').type('wrongpassword', { delay: 0 }); + cy.getDataTest('button-submit').click(); + cy.contains(/Incorrect username and\/or password./i).should('be.visible'); + }); }); diff --git a/cypress/e2e/user.cy.ts b/cypress/e2e/user.cy.ts new file mode 100644 index 00000000..9f432f16 --- /dev/null +++ b/cypress/e2e/user.cy.ts @@ -0,0 +1,65 @@ +describe('Website tests', () => { + Cypress.session.clearAllSavedSessions(); + + beforeEach(() => { + cy.login(Cypress.env('umami_user'), Cypress.env('umami_password')); + cy.visit('/settings/users'); + }); + + it('Add a User', () => { + // add user + cy.contains(/Create user/i).should('be.visible'); + cy.getDataTest('button-create-user').click(); + cy.getDataTest('input-username').find('input').as('inputName').click(); + cy.get('@inputName').type('Test-user', { delay: 0 }); + cy.getDataTest('input-password').find('input').as('inputPassword').click(); + cy.get('@inputPassword').type('testPasswordCypress', { delay: 0 }); + cy.getDataTest('dropdown-role').click(); + cy.getDataTest('dropdown-item-user').click(); + cy.getDataTest('button-submit').click(); + cy.get('td[label="Username"]').should('contain.text', 'Test-user'); + cy.get('td[label="Role"]').should('contain.text', 'User'); + }); + + it('Edit a User role and password', () => { + // edit user + cy.get('table tbody tr') + .contains('td', /Test-user/i) + .parent() + .within(() => { + cy.getDataTest('link-button-edit').click(); // Clicks the button inside the row + }); + cy.getDataTest('input-password').find('input').as('inputPassword').click(); + cy.get('@inputPassword').type('newPassword', { delay: 0 }); + cy.getDataTest('dropdown-role').click(); + cy.getDataTest('dropdown-item-viewOnly').click(); + cy.getDataTest('button-submit').click(); + + cy.visit('/settings/users'); + cy.get('table tbody tr') + .contains('td', /Test-user/i) + .parent() + .should('contain.text', 'View only'); + + cy.logout(); + cy.url().should('eq', Cypress.config().baseUrl + '/login'); + cy.getDataTest('input-username').find('input').as('inputUsername').click(); + cy.get('@inputUsername').type('Test-user', { delay: 0 }); + cy.get('@inputUsername').click(); + cy.getDataTest('input-password').find('input').type('newPassword', { delay: 0 }); + cy.getDataTest('button-submit').click(); + cy.url().should('eq', Cypress.config().baseUrl + '/dashboard'); + }); + + it('Delete a website', () => { + // delete user + cy.get('table tbody tr') + .contains('td', /Test-user/i) + .parent() + .within(() => { + cy.getDataTest('button-delete').click(); // Clicks the button inside the row + }); + cy.contains(/Are you sure you want to delete Test-user?/i).should('be.visible'); + cy.getDataTest('button-confirm').click(); + }); +}); diff --git a/cypress/e2e/website.cy.ts b/cypress/e2e/website.cy.ts index b60d8e7a..2dcd6027 100644 --- a/cypress/e2e/website.cy.ts +++ b/cypress/e2e/website.cy.ts @@ -10,10 +10,10 @@ describe('Website tests', () => { cy.visit('/settings/websites'); cy.getDataTest('button-website-add').click(); cy.contains(/Add website/i).should('be.visible'); - cy.getDataTest('input-name').find('input').click(); - cy.getDataTest('input-name').find('input').type('Add test', { delay: 50 }); + cy.getDataTest('input-name').find('input').as('inputUsername').click(); + cy.getDataTest('input-name').find('input').type('Add test', { delay: 0 }); cy.getDataTest('input-domain').find('input').click(); - cy.getDataTest('input-domain').find('input').type('addtest.com', { delay: 50 }); + cy.getDataTest('input-domain').find('input').type('addtest.com', { delay: 0 }); cy.getDataTest('button-submit').click(); cy.get('td[label="Name"]').should('contain.text', 'Add test'); cy.get('td[label="Domain"]').should('contain.text', 'addtest.com'); @@ -41,10 +41,10 @@ describe('Website tests', () => { cy.contains(/Details/i).should('be.visible'); cy.getDataTest('input-name').find('input').click(); cy.getDataTest('input-name').find('input').clear(); - cy.getDataTest('input-name').find('input').type('Updated website', { delay: 50 }); + cy.getDataTest('input-name').find('input').type('Updated website', { delay: 0 }); cy.getDataTest('input-domain').find('input').click(); cy.getDataTest('input-domain').find('input').clear(); - cy.getDataTest('input-domain').find('input').type('updatedwebsite.com', { delay: 50 }); + cy.getDataTest('input-domain').find('input').type('updatedwebsite.com', { delay: 0 }); cy.getDataTest('button-submit').click({ force: true }); cy.getDataTest('input-name').find('input').should('have.value', 'Updated website'); cy.getDataTest('input-domain').find('input').should('have.value', 'updatedwebsite.com'); diff --git a/cypress/fixtures/users.json b/cypress/fixtures/users.json new file mode 100644 index 00000000..420a71c3 --- /dev/null +++ b/cypress/fixtures/users.json @@ -0,0 +1,17 @@ +{ + "userGet": { + "name": "cypress", + "email": "password", + "role": "User" + }, + "userPost": { + "username": "cypress1", + "password": "password", + "role": "User" + }, + "userDelete": { + "name": "Charlie", + "email": "charlie@example.com", + "age": 35 + } +} diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 2c45142b..a300b969 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -5,6 +5,12 @@ Cypress.Commands.add('getDataTest', (value: string) => { return cy.get(`[data-test=${value}]`); }); +Cypress.Commands.add('logout', () => { + cy.getDataTest('button-profile').click(); + cy.getDataTest('item-logout').click(); + cy.url().should('eq', Cypress.config().baseUrl + '/login'); +}); + Cypress.Commands.add('login', (username: string, password: string) => { cy.session([username, password], () => { cy.request({ diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts index 90cca19b..e89b24dd 100644 --- a/cypress/support/index.d.ts +++ b/cypress/support/index.d.ts @@ -1,4 +1,5 @@ /// +/* global JQuery */ declare namespace Cypress { interface Chainable { @@ -7,6 +8,11 @@ declare namespace Cypress { * @example cy.getDataTest('greeting') */ getDataTest(value: string): Chainable>; + /** + * Custom command to logout through UI. + * @example cy.logout() + */ + logout(): Chainable>; /** * Custom command to login user into the app. * @example cy.login('admin', 'password) diff --git a/src/app/(main)/settings/users/UserAddButton.tsx b/src/app/(main)/settings/users/UserAddButton.tsx index e1b04842..674771b6 100644 --- a/src/app/(main)/settings/users/UserAddButton.tsx +++ b/src/app/(main)/settings/users/UserAddButton.tsx @@ -15,7 +15,7 @@ export function UserAddButton({ onSave }: { onSave?: () => void }) { return ( - diff --git a/src/lib/request.ts b/src/lib/request.ts index 0c71537a..374f1cbc 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -1,4 +1,4 @@ -import { ZodSchema } from 'zod'; +import { z, ZodSchema } from 'zod'; import { FILTER_COLUMNS } from '@/lib/constants'; import { badRequest, unauthorized } from '@/lib/response'; import { getAllowedUnits, getMinimumUnit } from '@/lib/date'; @@ -24,12 +24,21 @@ export async function parseRequest( let error: () => void | undefined; let auth = null; + const getErrorMessages = (error: z.ZodError) => { + return Object.entries(error.format()) + .map(([key, value]) => { + const messages = (value as any)._errors; + return messages ? `${key}: ${messages.join(', ')}` : null; + }) + .filter(Boolean); + }; + if (schema) { const isGet = request.method === 'GET'; const result = schema.safeParse(isGet ? query : body); if (!result.success) { - error = () => badRequest(result.error); + error = () => badRequest(getErrorMessages(result.error)); } else if (isGet) { query = result.data; } else {