Browse Source

test(cypress): WIP - add tests for api token and user management

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/4134/head
Pranav C 2 years ago
parent
commit
d10ac2546f
  1. 15
      packages/nc-gui/components/account/SignupSettings.vue
  2. 7
      packages/nc-gui/components/account/Token.vue
  3. 8
      packages/nc-gui/components/account/UserList.vue
  4. 6
      packages/nc-gui/components/account/UsersModal.vue
  5. 1
      packages/nc-gui/lang/en.json
  6. 11
      packages/nc-gui/pages/account/index.vue
  7. 9
      packages/nc-gui/pages/account/index/[page].vue
  8. 68
      packages/nocodb/src/lib/meta/api/orgTokenApis.ts
  9. 2
      packages/nocodb/src/lib/meta/helpers/apiMetrics.ts
  10. 140
      scripts/cypress/integration/common/5c_super_user_role.js

15
packages/nc-gui/components/account/SignupSettings.vue

@ -4,12 +4,11 @@ import { extractSdkResponseErrorMsg, useApi } from '#imports'
const { api } = useApi()
let settings = $ref({ disable_user_signup: false })
let settings = $ref<{ disable_user_signup?: boolean }>({ disable_user_signup: false })
const loadSettings = async () => {
try {
const response = await api.orgAppSettings.get()
settings = response
} catch (e) {
message.error(await extractSdkResponseErrorMsg(e))
@ -33,9 +32,15 @@ loadSettings()
<div class="text-xl">Settings</div>
<a-divider class="!my-3" />
<a-form-item>
<a-checkbox v-model:checked="settings.disable_user_signup" name="virtual" @change="saveSettings"
>Disable user signup</a-checkbox
>
<a-checkbox class="nc-checkbox" v-model:checked="settings.disable_user_signup" name="virtual"
@change="saveSettings">
Disable user signup
</a-checkbox>
</a-form-item>
</div>
</template>
<style scoped>
:deep(.nc-checkbox label) {
@apply flex-row-reverse !flex;
}
</style>

7
packages/nc-gui/components/account/Token.vue

@ -100,11 +100,11 @@ const descriptionInput = ref((el) => {
<div class="h-full overflow-y-scroll scrollbar-thin-dull pt-2">
<div class="text-xl mt-4">Token Management</div>
<a-divider class="!my-3" />
<div class="max-w-[900px] mx-auto p-4">
<div class="max-w-[900px] mx-auto p-4" data-cy="nc-token-list">
<div class="py-2 flex gap-4 items-center">
<div class="flex-grow"></div>
<MdiReload class="cursor-pointer" @click="loadTokens" />
<a-button size="small" @click="showNewTokenModal = true">
<a-button data-cy="nc-token-create" size="small" type="primary" @click="showNewTokenModal = true">
<div class="flex items-center gap-1">
<MdiAdd />
Add new token
@ -242,6 +242,7 @@ const descriptionInput = ref((el) => {
@finish="generateToken"
>
<a-input
data-cy="nc-token-modal-description"
:ref="descriptionInput"
v-model:value="selectedTokenData.description"
:placeholder="$t('labels.description')"
@ -249,7 +250,7 @@ const descriptionInput = ref((el) => {
<!-- Generate -->
<div class="flex flex-row justify-center">
<a-button type="primary" html-type="submit">
<a-button type="primary" html-type="submit" data-cy="nc-token-modal-save">
{{ $t('general.generate') }}
</a-button>
</div>

8
packages/nc-gui/components/account/UserList.vue

@ -119,7 +119,7 @@ const copyPasswordResetUrl = async (user: User) => {
</script>
<template>
<div>
<div data-cy='nc-super-user-list'>
<div class="text-xl">User Management</div>
<a-divider class="!my-3" />
<div class="max-w-[900px] mx-auto p-4">
@ -135,7 +135,7 @@ const copyPasswordResetUrl = async (user: User) => {
</a-input-search>
<div class="flex-grow"></div>
<MdiReload class="cursor-pointer" @click="loadUsers" />
<a-button size="small" @click="showUserModal = true">
<a-button data-cy="nc-super-user-invite" size="small" type="primary" @click="showUserModal = true">
<div class="flex items-center gap-1">
<MdiAdd />
Invite new user
@ -164,7 +164,7 @@ const copyPasswordResetUrl = async (user: User) => {
</a-table-column>
<!-- Role -->
<a-table-column key="roles" :title="$t('objects.role')" data-index="roles">
<a-table-column key="roles" :title="$t('objects.role')" data-index="roles">
<template #default="{ record }">
<div>
<div v-if="record.roles.includes('super')" class="font-weight-bold">Super Admin</div>
@ -207,7 +207,7 @@ const copyPasswordResetUrl = async (user: User) => {
<a-table-column key="id" :title="$t('labels.actions')" data-index="id">
<template #default="{ text, record }">
<div v-if="!record.roles.includes('super')" class="flex items-center gap-2">
<MdiDeleteOutline class="nc-action-btn cursor-pointer" @click="deleteUser(text)" />
<MdiDeleteOutline data-cy="nc-super-user-delete" class="nc-action-btn cursor-pointer" @click="deleteUser(text)" />
<a-dropdown :trigger="['click']" class="flex" placement="bottomRight" overlay-class-name="nc-dropdown-user-mgmt">
<div class="flex flex-row items-center">

6
packages/nc-gui/components/account/UsersModal.vue

@ -82,7 +82,7 @@ const saveUser = async () => {
emit('reload')
// Successfully updated the user details
message.success(t('msg.success.userDetailsUpdated'))
message.success(t('msg.success.userAdded'))
} catch (e: any) {
console.error(e)
message.error(await extractSdkResponseErrorMsg(e))
@ -120,7 +120,7 @@ const emailInput = ref((el) => {
:visible="show"
:closable="false"
width="max(50vw, 44rem)"
wrap-class-name="nc-modal-invite-user-and-share-base"
wrap-class-name="nc-modal-invite-user"
@cancel="emit('closed')"
>
<div class="flex flex-col">
@ -129,7 +129,7 @@ const emailInput = ref((el) => {
<a-button type="text" class="!rounded-md mr-1 -mt-1.5" @click="emit('closed')">
<template #icon>
<MaterialSymbolsCloseRounded class="flex mx-auto" />
<MaterialSymbolsCloseRounded data-cy="nc-root-user-invite-modal-close" class="flex mx-auto" />
</template>
</a-button>
</div>

1
packages/nc-gui/lang/en.json

@ -692,6 +692,7 @@
"tokenGenerated": "Token generated successfully",
"tokenDeleted": "Token deleted successfully",
"userAddedToProject": "Successfully added user to project",
"userAdded": "Successfully added user",
"userDeletedFromProject": "Successfully deleted user from project",
"inviteEmailSent": "Invite Email sent successfully",
"inviteURLCopied": "Invite URL copied to clipboard",

11
packages/nc-gui/pages/account/index.vue

@ -38,17 +38,6 @@ const { isUIAllowed } = useUIPermission()
<div class="select-none">Tokens</div>
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('appLicense')"
key="license"
class="group active:(!ring-0) hover:(!bg-primary !bg-opacity-25)"
@click="navigateTo('/account/license')"
>
<div class="flex items-center space-x-2">
<MdiKeyChainVariant />
<div class="select-none">License</div>
</div>
</a-menu-item>
</a-menu>
</div>
</a-layout-sider>

9
packages/nc-gui/pages/account/index/[page].vue

@ -1,14 +1,5 @@
<script>
export default {
name: 'Index',
}
</script>
<template>
<AccountUserManagement v-if="$route.params.page === 'users'" />
<AccountToken v-else-if="$route.params.page === 'tokens'" />
<AccountLicense v-else-if="$route.params.page === 'license'" />
<span v-else></span>
</template>
<style scoped></style>

68
packages/nocodb/src/lib/meta/api/orgTokenApis.ts

@ -1,54 +1,58 @@
import { Request, Response, Router } from 'express'
import { OrgUserRoles } from '../../../enums/OrgUserRoles'
import ApiToken from '../../models/ApiToken'
import { Tele } from '../../utils/Tele'
import { metaApiMetrics } from '../helpers/apiMetrics'
import { NcError } from '../helpers/catchError'
import getHandler from '../helpers/getHandler'
import ncMetaAclMw from '../helpers/ncMetaAclMw'
import { PagedResponseImpl } from '../helpers/PagedResponse'
import { apiTokenListEE } from './ee/orgTokenApis'
import { Request, Response, Router } from 'express';
import { OrgUserRoles } from '../../../enums/OrgUserRoles';
import ApiToken from '../../models/ApiToken';
import { Tele } from '../../utils/Tele';
import { metaApiMetrics } from '../helpers/apiMetrics';
import { NcError } from '../helpers/catchError';
import getHandler from '../helpers/getHandler';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import { apiTokenListEE } from './ee/orgTokenApis';
async function apiTokenList(req, res) {
const fk_user_id = req.user.id
let includeUnmappedToken = false
const fk_user_id = req.user.id;
let includeUnmappedToken = false;
if (req['user'].roles.includes(OrgUserRoles.SUPER)) {
includeUnmappedToken = true
includeUnmappedToken = true;
}
res.json(
new PagedResponseImpl(
await ApiToken.listWithCreatedBy({ ...req.query, fk_user_id, includeUnmappedToken }),
await ApiToken.listWithCreatedBy({
...req.query,
fk_user_id,
includeUnmappedToken,
}),
{
...req.query,
count: await ApiToken.count({
includeUnmappedToken,
fk_user_id,
}),
},
),
)
}
)
);
}
export async function apiTokenCreate(req: Request, res: Response) {
Tele.emit('evt', { evt_type: 'org:apiToken:created' })
res.json(await ApiToken.insert({ ...req.body, fk_user_id: req['user'].id }))
Tele.emit('evt', { evt_type: 'org:apiToken:created' });
res.json(await ApiToken.insert({ ...req.body, fk_user_id: req['user'].id }));
}
export async function apiTokenDelete(req: Request, res: Response) {
const fk_user_id = req['user'].id
const apiToken = await ApiToken.getByToken(req.params.apiTokenId)
const fk_user_id = req['user'].id;
const apiToken = await ApiToken.getByToken(req.params.apiTokenId);
if (
!req['user'].roles.includes(OrgUserRoles.SUPER) &&
apiToken.fk_user_id !== fk_user_id
) {
NcError.notFound('Token not found')
NcError.notFound('Token not found');
}
Tele.emit('evt', { evt_type: 'org:apiToken:deleted' })
res.json(await ApiToken.delete(req.params.token))
Tele.emit('evt', { evt_type: 'org:apiToken:deleted' });
res.json(await ApiToken.delete(req.params.token));
}
const router = Router({ mergeParams: true })
const router = Router({ mergeParams: true });
router.get(
'/api/v1/tokens',
@ -56,22 +60,22 @@ router.get(
ncMetaAclMw(getHandler(apiTokenList, apiTokenListEE), 'apiTokenList', {
// allowedRoles: [OrgUserRoles.SUPER],
blockApiTokenAccess: true,
}),
)
})
);
router.post(
'/api/v1/tokens',
metaApiMetrics,
ncMetaAclMw(apiTokenCreate, 'apiTokenCreate', {
// allowedRoles: [OrgUserRoles.SUPER],
blockApiTokenAccess: true,
}),
)
})
);
router.delete(
'/api/v1/tokens/:token',
metaApiMetrics,
ncMetaAclMw(apiTokenDelete, 'apiTokenDelete', {
// allowedRoles: [OrgUserRoles.SUPER],
blockApiTokenAccess: true,
}),
)
export default router
})
);
export default router;

2
packages/nocodb/src/lib/meta/helpers/apiMetrics.ts

@ -14,7 +14,7 @@ const metrics = async (req: Request, c = 150) => {
};
const metaApiMetrics = (req: Request, _res, next) => {
metrics(req, 10).then(() => {});
metrics(req, 50).then(() => {});
next();
};
export default (req: Request, _res, next) => {

140
scripts/cypress/integration/common/5c_super_user_role.js

@ -1,9 +1,10 @@
import { loginPage } from "../../support/page_objects/navigation";
import { roles } from "../../support/page_objects/projectConstants";
import { loginPage } from '../../support/page_objects/navigation';
import { roles } from '../../support/page_objects/projectConstants';
export const genTest = (apiType, dbType) => {
describe(`${apiType.toUpperCase()} api - Super user test`, () => {
before(() => {});
before(() => {
});
beforeEach(() => {
cy.restoreLocalStorage();
@ -13,71 +14,144 @@ export const genTest = (apiType, dbType) => {
cy.saveLocalStorage();
});
after(() => {});
after(() => {
});
it(`Open App store page and check slack app`, () => {
cy.visit("/#/apps").then((win) => {
cy.get(".nc-app-store-title").should("exist");
cy.get(".nc-app-store-card-Slack").should("exist");
cy.visit('/#/apps').then((win) => {
cy.get('.nc-app-store-title').should('exist');
cy.get('.nc-app-store-card-Slack').should('exist');
// install slack app
cy.get(".nc-app-store-card-Slack .install-btn").invoke(
"attr",
"style",
"right: 10px"
cy.get('.nc-app-store-card-Slack .install-btn').invoke(
'attr',
'style',
'right: 10px'
);
cy.get(
".nc-app-store-card-Slack .install-btn .nc-app-store-card-install"
'.nc-app-store-card-Slack .install-btn .nc-app-store-card-install'
).click();
cy.getActiveModal(".nc-modal-plugin-install")
cy.getActiveModal('.nc-modal-plugin-install')
.find('[placeholder="Channel Name"]')
.type("Test channel");
.type('Test channel');
cy.getActiveModal(".nc-modal-plugin-install")
cy.getActiveModal('.nc-modal-plugin-install')
.find('[placeholder="Webhook URL"]')
.type("http://test.com");
.type('http://test.com');
cy.getActiveModal(".nc-modal-plugin-install")
cy.getActiveModal('.nc-modal-plugin-install')
.find('button:contains("Save")')
.click();
cy.toastWait("Successfully installed");
cy.toastWait('Successfully installed');
cy.get(
".nc-app-store-card-Slack .install-btn .nc-app-store-card-install"
).should("not.exist");
'.nc-app-store-card-Slack .install-btn .nc-app-store-card-install'
).should('not.exist');
// update slack app config
cy.get(".nc-app-store-card-Slack .install-btn .nc-app-store-card-edit")
.should("exist")
cy.get('.nc-app-store-card-Slack .install-btn .nc-app-store-card-edit')
.should('exist')
.click();
cy.getActiveModal(".nc-modal-plugin-install")
.should("exist")
cy.getActiveModal('.nc-modal-plugin-install')
.should('exist')
.find('[placeholder="Channel Name"]')
.should("have.value", "Test channel")
.should('have.value', 'Test channel')
.clear()
.type("Test channel 2");
.type('Test channel 2');
cy.getActiveModal(".nc-modal-plugin-install")
cy.getActiveModal('.nc-modal-plugin-install')
.get('button:contains("Save")')
.click();
cy.toastWait("Successfully installed");
cy.toastWait('Successfully installed');
// reset slack app
cy.get(".nc-app-store-card-Slack .install-btn .nc-app-store-card-reset")
.should("exist")
cy.get('.nc-app-store-card-Slack .install-btn .nc-app-store-card-reset')
.should('exist')
.click();
cy.getActiveModal(".nc-modal-plugin-uninstall")
.should("exist")
cy.getActiveModal('.nc-modal-plugin-uninstall')
.should('exist')
.find('button:contains("Confirm")')
.click();
cy.toastWait("Plugin uninstalled successfully");
cy.toastWait('Plugin uninstalled successfully');
});
});
it(`Open super user management page and add/delete user`, () => {
cy.wait(500);
cy.visit('/#/account/users').then((win) => {
cy.get('[data-cy="nc-super-user-list"]').should('exist')
.find('tbody tr').should('have.length', 1);
cy.get('[data-cy=\'nc-super-user-invite\'')
.click();
// additional wait to ensure the modal is fully loaded
cy.getActiveModal('.nc-modal-invite-user').should('exist');
cy.getActiveModal('.nc-modal-invite-user')
.find('input[placeholder="E-mail"]')
.should('exist');
cy.getActiveModal('.nc-modal-invite-user')
.find('input[placeholder="E-mail"]')
.type('test@nocodb.com');
cy.getActiveModal('.nc-modal-invite-user')
.find('.ant-select.nc-user-roles')
.click();
cy.getActiveModal('.nc-modal-invite-user')
.find('button.ant-btn-primary')
.click();
cy.toastWait('Successfully added user');
cy.getActiveModal().find('[data-cy="nc-root-user-invite-modal-close"]').click();
cy.get('[data-cy="nc-super-user-list"]').should('exist')
.find('tbody tr').should('have.length', 2)
.last().find('[data-cy="nc-super-user-delete"]').click();
cy.getActiveModal().find('.ant-modal-confirm-btns .ant-btn-primary').click();
cy.toastWait('User deleted successfully');
cy.get('[data-cy="nc-super-user-list"]').should('exist')
.find('tbody tr').should('have.length', 1);
});
});
it('User management settings', () => {
});
it(`Token management`, () => {
cy.wait(500);
cy.visit('/#/account/tokens').then((win) => {
cy.get('[data-cy="nc-token-list"]').should('exist').find(':contains("No Data")').should('exist');
cy.get('[data-cy="nc-token-create"]').click();
cy.get('[data-cy="nc-token-modal-description"]').type('Descriptqion');
cy.get('[data-cy="nc-token-modal-save"]').click();
cy.toastWait('Token created successfully');
cy.get('[data-cy="nc-token-list"]').should('exist')
.find('tbody tr').should('have.length', 1);
});
});
});
};

Loading…
Cancel
Save