Browse Source

Merge branch 'develop' into enhancement/filters

pull/5106/head
Wing-Kam Wong 2 years ago
parent
commit
9969213acd
  1. BIN
      packages/nc-gui/assets/img/brand/nocodb-full-color.png
  2. 8
      packages/nc-gui/assets/style.scss
  3. 1
      packages/nc-gui/components.d.ts
  4. 4
      packages/nc-gui/components/cell/SingleSelect.vue
  5. 8
      packages/nc-gui/components/dashboard/TreeView.vue
  6. 9
      packages/nc-gui/components/general/ShareBaseButton.vue
  7. 2
      packages/nc-gui/components/smartsheet/Pagination.vue
  8. 2
      packages/nc-gui/components/smartsheet/sidebar/toolbar/ToggleDrawer.vue
  9. 2
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue
  10. 2
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  11. 8
      packages/nc-gui/components/smartsheet/toolbar/SearchData.vue
  12. 2
      packages/nc-gui/components/smartsheet/toolbar/ShareView.vue
  13. 2
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  14. 2
      packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue
  15. 15
      packages/nc-gui/layouts/base.vue
  16. 2
      packages/nc-gui/package-lock.json
  17. 12
      packages/nc-gui/pages/[projectType]/[projectId]/index.vue
  18. 58
      packages/nc-gui/pages/[projectType]/[projectId]/index/index.vue
  19. 2
      packages/nc-lib-gui/package.json
  20. 4
      packages/nocodb-sdk/package-lock.json
  21. 2
      packages/nocodb-sdk/package.json
  22. 20
      packages/nocodb/package-lock.json
  23. 4
      packages/nocodb/package.json
  24. 4
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/CustomKnex.ts
  25. 138
      packages/nocodb/tests/unit/rest/tests/attachment.test.ts
  26. 33
      packages/nocodb/tests/unit/rest/tests/auth.test.ts
  27. 6
      packages/nocodb/tests/unit/rest/tests/columnTypeSpecific.test.ts
  28. 201
      packages/nocodb/tests/unit/rest/tests/org.test.ts
  29. 251
      packages/nocodb/tests/unit/rest/tests/project.test.ts
  30. 12
      packages/nocodb/tests/unit/rest/tests/table.test.ts
  31. 114
      packages/nocodb/tests/unit/rest/tests/tableRow.test.ts
  32. 70
      packages/nocodb/tests/unit/rest/tests/viewRow.test.ts

BIN
packages/nc-gui/assets/img/brand/nocodb-full-color.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

8
packages/nc-gui/assets/style.scss

@ -6,6 +6,8 @@
--header-height: 42px;
--toolbar-height: 48px;
--tw-text-opacity: 1;
--navbar-bg: #FAFAFA;
--navbar-border: #e0e0e0;
}
.ant-layout-header {
@ -303,3 +305,9 @@ a {
@apply text-gray-300 italic;
}
}
.ant-btn-loading-icon{
& > span {
@apply block bg-red-500
}
}

1
packages/nc-gui/components.d.ts vendored

@ -81,6 +81,7 @@ declare module '@vue/runtime-core' {
ClaritySuccessLine: typeof import('~icons/clarity/success-line')['default']
EvaEmailOutline: typeof import('~icons/eva/email-outline')['default']
IcBaselineMoreVert: typeof import('~icons/ic/baseline-more-vert')['default']
Icon: typeof import('~icons/ic/on')['default']
IcOutlineInsertDriveFile: typeof import('~icons/ic/outline-insert-drive-file')['default']
IcRoundEdit: typeof import('~icons/ic/round-edit')['default']
IcRoundKeyboardArrowDown: typeof import('~icons/ic/round-keyboard-arrow-down')['default']

4
packages/nc-gui/components/cell/SingleSelect.vue

@ -330,4 +330,8 @@ useEventListener(document, 'click', handleClose, true)
:deep(.ant-select-selection-search-input) {
@apply !text-xs;
}
:deep(.ant-select-clear > span) {
@apply block;
}
</style>

8
packages/nc-gui/components/dashboard/TreeView.vue

@ -360,8 +360,8 @@ onMounted(async () => {
</Transition>
<Transition name="layout" mode="out-in">
<MdiClose v-if="searchActive" class="text-lg mx-1 mt-0.5" @click="onSearchCloseIconClick" />
<IcRoundSearch v-else class="text-lg text-primary mx-1 mt-0.5" @click="toggleSearchActive(true)" />
<MdiClose v-if="searchActive" class="text-gray-500 text-lg mx-1 mt-0.5" @click="onSearchCloseIconClick" />
<IcRoundSearch v-else class="text-gray-500 text-lg mx-1 mt-0.5" @click="toggleSearchActive(true)" />
</Transition>
</div>
<div
@ -387,8 +387,8 @@ onMounted(async () => {
</Transition>
<Transition name="slide-right" mode="out-in">
<MdiClose v-if="searchActive" class="text-lg mx-1 mt-0.5" @click="onSearchCloseIconClick" />
<IcRoundSearch v-else class="text-lg text-primary mx-1 mt-0.5" @click="onSearchIconClick" />
<MdiClose v-if="searchActive" class="text-gray-500 text-lg mx-1 mt-0.5" @click="onSearchCloseIconClick" />
<IcRoundSearch v-else class="text-gray-500 text-lg mx-1 mt-0.5" @click="onSearchIconClick" />
</Transition>
<a-dropdown v-if="!isSharedBase" :trigger="['click']" overlay-class-name="nc-dropdown-import-menu" @click.stop>

9
packages/nc-gui/components/general/ShareBaseButton.vue

@ -43,9 +43,12 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<template #title>
<span class="text-xs">{{ $t('activity.inviteTeam') }}</span>
</template>
<div class="flex items-center space-x-1 cursor-pointer">
<MdiAccountPlusOutline class="mr-1 nc-share-base text-gray-300 hover:text-accent" />
</div>
<a-button type="primary" class="!rounded-md mr-1" size="medium">
<div class="flex items-center space-x-1 cursor-pointer text-xs font-weight-bold">
<MdiAccountPlusOutline class="mr-1 nc-share-base hover:text-accent text-sm" />
{{ $t('activity.share') }}
</div>
</a-button>
</a-tooltip>
</div>

2
packages/nc-gui/components/smartsheet/Pagination.vue

@ -59,6 +59,6 @@ const page = computed({
}
:deep(.ant-pagination-item-link) {
@apply text-gray-500;
@apply text-gray-500 flex items-center justify-center;
}
</style>

2
packages/nc-gui/components/smartsheet/sidebar/toolbar/ToggleDrawer.vue

@ -11,7 +11,7 @@ const onClick = () => {
<template>
<div :class="{ 'nc-active-btn': isOpen }">
<a-button size="small" class="nc-toggle-right-navbar" @click="onClick">
<div class="flex items-center gap-1 text-xs" :class="{ 'text-gray-500': !isOpen }">
<div class="flex items-center gap-1 text-[0.6rem] text-gray-500" :class="{ 'text-gray-500': !isOpen }">
<AntDesignMenuUnfoldOutlined v-if="isOpen" />
<AntDesignMenuFoldOutlined v-else />

2
packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue

@ -76,7 +76,7 @@ useMenuCloseOnEsc(open)
<div class="flex items-center gap-1">
<MdiFilterOutline />
<!-- Filter -->
<span class="text-capitalize !text-sm font-weight-normal">{{ $t('activity.filter') }}</span>
<span class="text-capitalize !text-xs font-weight-normal">{{ $t('activity.filter') }}</span>
<MdiMenuDown class="text-grey" />
<span v-if="filtersLength" class="nc-count-badge">{{ filtersLength }}</span>

2
packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue

@ -151,7 +151,7 @@ useMenuCloseOnEsc(open)
<MdiEyeOffOutline />
<!-- Fields -->
<span class="text-capitalize !text-sm font-weight-normal">{{ $t('objects.fields') }}</span>
<span class="text-capitalize !text-xs font-weight-normal">{{ $t('objects.fields') }}</span>
<MdiMenuDown class="text-grey" />

8
packages/nc-gui/components/smartsheet/toolbar/SearchData.vue

@ -73,7 +73,7 @@ function onPressEnter() {
<a-input
v-model:value="search.query"
size="small"
class="max-w-[200px]"
class="max-w-[200px] !text-xs"
:placeholder="$t('placeholder.filterQuery')"
:bordered="false"
data-testid="search-data-input"
@ -83,3 +83,9 @@ function onPressEnter() {
</a-input>
</div>
</template>
<style scoped>
:deep(input::placeholder) {
@apply !text-xs;
}
</style>

2
packages/nc-gui/components/smartsheet/toolbar/ShareView.vue

@ -237,7 +237,7 @@ const copyIframeCode = async () => {
<div class="flex items-center gap-1">
<MdiOpenInNew />
<!-- Share View -->
<span class="!text-sm font-weight-normal"> {{ $t('activity.shareView') }}</span>
<span class="!text-xs font-weight-normal"> {{ $t('activity.shareView') }}</span>
</div>
</a-button>

2
packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue

@ -66,7 +66,7 @@ useMenuCloseOnEsc(open)
<MdiSort />
<!-- Sort -->
<span class="text-capitalize !text-sm font-weight-normal">{{ $t('activity.sort') }}</span>
<span class="text-capitalize !text-xs font-weight-normal">{{ $t('activity.sort') }}</span>
<MdiMenuDown class="text-grey" />
<span v-if="sorts?.length" class="nc-count-badge">{{ sorts.length }}</span>

2
packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue

@ -94,7 +94,7 @@ useMenuCloseOnEsc(open)
<div class="flex gap-2 items-center">
<GeneralViewIcon :meta="selectedView"></GeneralViewIcon>
<span class="!text-sm font-weight-normal">
<span class="!text-xs font-weight-normal">
<GeneralTruncateText :key="selectedView?.title">{{ selectedView?.title }}</GeneralTruncateText>
</span>

15
packages/nc-gui/layouts/base.vue

@ -37,10 +37,7 @@ hooks.hook('page:finish', () => {
</Transition>
<a-layout class="!flex-col">
<a-layout-header
v-if="!route.meta.public && signedIn && !route.meta.hideHeader"
class="flex !bg-primary items-center text-white !pl-2 !pr-5"
>
<a-layout-header v-if="!route.meta.public && signedIn && !route.meta.hideHeader" class="nc-navbar">
<div
v-if="!route.params.projectType"
v-e="['c:navbar:home']"
@ -53,8 +50,8 @@ hooks.hook('page:finish', () => {
{{ currentVersion }}
</template>
<div class="flex items-center gap-2">
<img width="25" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
<img v-show="!isDashboard" width="90" alt="NocoDB" src="~/assets/img/brand/text.png" />
<img v-if="!isDashboard" width="120" alt="NocoDB" src="~/assets/img/brand/nocodb-full-color.png" />
<img v-else width="25" alt="NocoDB" src="~/assets/img/icons/512x512.png" />
</div>
</a-tooltip>
</div>
@ -83,7 +80,7 @@ hooks.hook('page:finish', () => {
<a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-user-accounts-menu">
<MdiDotsVertical
data-testid="nc-menu-accounts"
class="md:text-xl cursor-pointer hover:text-accent nc-menu-accounts text-white"
class="md:text-xl cursor-pointer hover:text-accent nc-menu-accounts"
@click.prevent
/>
@ -161,4 +158,8 @@ hooks.hook('page:finish', () => {
@apply ring ring-accent ring-opacity-100;
}
}
.nc-navbar {
@apply flex !bg-white items-center !pl-2 !pr-5;
}
</style>

2
packages/nc-gui/package-lock.json generated

@ -97,7 +97,7 @@
}
},
"../nocodb-sdk": {
"version": "0.104.2",
"version": "0.104.3",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",

12
packages/nc-gui/pages/[projectType]/[projectId]/index.vue

@ -252,9 +252,9 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
theme="light"
>
<div
style="height: var(--header-height)"
style="height: var(--header-height); border-bottom-width: 1px"
:class="isOpen ? 'pl-4' : ''"
class="flex items-center !bg-primary text-white px-1 gap-1"
class="flex items-center text-primary px-1 gap-1 nc-sidebar-header"
>
<div
v-if="isOpen && !isSharedBase"
@ -267,7 +267,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<template #title>
{{ currentVersion }}
</template>
<img width="25" class="-mr-1" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
<img width="25" class="-mr-1" alt="NocoDB" src="~/assets/img/icons/512x512.png" />
</a-tooltip>
</div>
@ -589,7 +589,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
.nc-left-sidebar {
.nc-sidebar-left-toggle-icon {
@apply opacity-0 transition-opactity duration-200 transition-color text-white/80 hover:text-white/100;
@apply opacity-0 transition-opactity duration-200 transition-color text-gray-500/80 hover:text-gray-500/100;
.nc-left-sidebar {
@apply !border-r-0;
@ -604,4 +604,8 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
:deep(.ant-dropdown-menu-submenu-title) {
@apply py-0;
}
.nc-sidebar-header {
@apply border-[var(--navbar-border)] !bg-[var(--navbar-bg)];
}
</style>

58
packages/nc-gui/pages/[projectType]/[projectId]/index/index.vue

@ -31,20 +31,20 @@ function onEdit(targetKey: number, action: 'add' | 'remove' | string) {
<template>
<div class="h-full w-full nc-container">
<div class="h-full w-full flex flex-col">
<div class="flex items-end !min-h-[var(--header-height)] !bg-primary nc-tab-bar">
<div class="flex items-end !min-h-[var(--header-height)] !bg-white-500 nc-tab-bar">
<div
v-if="!isOpen"
class="nc-sidebar-left-toggle-icon hover:after:(bg-primary bg-opacity-75) group nc-sidebar-add-row py-2 px-3"
class="nc-sidebar-left-toggle-icon hover:after:(bg-primary bg-opacity-75) group nc-sidebar-add-row py-2 px-3 mb-1"
>
<MdiMenu
v-e="['c:grid:toggle-navdraw']"
class="cursor-pointer transform transition-transform duration-500 text-white"
class="cursor-pointer transform transition-transform duration-500 text-gray-500/80 hover:text-gray-500"
:class="{ 'rotate-180': !isOpen }"
@click="toggle(!isOpen)"
/>
</div>
<a-tabs v-model:activeKey="activeTabIndex" class="nc-root-tabs" type="editable-card" @edit="onEdit">
<a-tabs v-model:activeKey="activeTabIndex" class="nc-root-tabs min-w-[500px]" type="editable-card" @edit="onEdit">
<a-tab-pane v-for="(tab, i) of tabs" :key="i">
<template #tab>
<div class="flex items-center gap-2" data-testid="nc-tab-title">
@ -78,8 +78,8 @@ function onEdit(targetKey: number, action: 'add' | 'remove' | string) {
</div>
</div>
<LazyGeneralShareBaseButton />
<LazyGeneralFullScreen class="nc-fullscreen-icon" />
<LazyGeneralShareBaseButton class="mb-1px" />
<LazyGeneralFullScreen class="nc-fullscreen-icon mb-1px" />
</div>
<div class="w-full min-h-[300px] flex-auto">
@ -108,23 +108,39 @@ function onEdit(targetKey: number, action: 'add' | 'remove' | string) {
}
.ant-tabs-nav-more {
@apply text-white;
@apply py-1.5;
}
& > .ant-tabs-nav-wrap > .ant-tabs-nav-list {
& > .ant-tabs-tab-active {
@apply font-weight-medium;
& > .ant-tabs-tab {
@apply border-0 !text-sm py-2 font-weight-medium z-2;
border-top-right-radius: 8px;
border-top-left-radius: 8px;
& + .ant-tabs-tab {
@apply ml-1;
}
}
& > .ant-tabs-tab {
@apply border-0;
& > .ant-tabs-tab-active {
@apply relative bg-white w-full h-full overflow-y-visible;
border-top: 1px solid white;
border-left: 1px solid white;
border-right: 1px solid white;
@apply !border-[var(--navbar-border)];
&:after {
@apply absolute content-[''] left-0 -bottom-[1px] w-full h-[1px] bg-inherit z-100;
}
}
& > .ant-tabs-tab:not(.ant-tabs-tab-active) {
//@apply bg-gray-100 text-gray-500;
@apply bg-white/10 text-white/90;
@apply bg-gray-50 text-gray-500;
.ant-tabs-tab-remove {
@apply !text-white;
@apply !text-default;
}
}
}
@ -141,7 +157,17 @@ function onEdit(targetKey: number, action: 'add' | 'remove' | string) {
@apply !border-none;
}
:deep(.ant-tabs-tab-remove) {
@apply flex mt-[2px];
.nc-tab-bar {
@apply border-gray-150 !bg-gray-50 relative z-1;
:deep(.ant-tabs-tab-remove) {
@apply flex mt-[2px];
}
background: linear-gradient(0deg, var(--navbar-border) 1px, var(--navbar-bg) 1px) !important;
:deep(.ant-tabs-tab:not(.ant-tabs-tab-active)) {
background: linear-gradient(0deg, var(--navbar-border) 1px, #f2f2f2 1px) !important;
}
}
</style>

2
packages/nc-lib-gui/package.json

@ -1,6 +1,6 @@
{
"name": "nc-lib-gui",
"version": "0.104.2",
"version": "0.104.3",
"description": "NocoDB GUI",
"author": {
"name": "NocoDB",

4
packages/nocodb-sdk/package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "nocodb-sdk",
"version": "0.104.2",
"version": "0.104.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nocodb-sdk",
"version": "0.104.2",
"version": "0.104.3",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",

2
packages/nocodb-sdk/package.json

@ -1,6 +1,6 @@
{
"name": "nocodb-sdk",
"version": "0.104.2",
"version": "0.104.3",
"description": "NocoDB SDK",
"main": "build/main/index.js",
"typings": "build/main/index.d.ts",

20
packages/nocodb/package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "nocodb",
"version": "0.104.2",
"version": "0.104.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nocodb",
"version": "0.104.2",
"version": "0.104.3",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@google-cloud/storage": "^5.7.2",
@ -65,7 +65,7 @@
"mysql2": "^2.2.5",
"nanoid": "^3.1.20",
"nc-help": "0.2.85",
"nc-lib-gui": "0.104.2",
"nc-lib-gui": "0.104.3",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",
@ -153,7 +153,7 @@
}
},
"../nocodb-sdk": {
"version": "0.104.2",
"version": "0.104.3",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
@ -11161,9 +11161,9 @@
}
},
"node_modules/nc-lib-gui": {
"version": "0.104.2",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.104.2.tgz",
"integrity": "sha512-B4FmViGweFoOieL0mFXdiUWV4GtOQo7XCun2so2So78pJD49yIq6d3oxN8+xiNyGNTvsophcCU9xchV8NLK9/Q==",
"version": "0.104.3",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.104.3.tgz",
"integrity": "sha512-gQjq2Yg5vYaai/RiRDrdrWlw+qqglpha0tHuZQ/xGwSuAO9Gn/v62YeeI0DAw+wvzR8CL+p/B5CQcz1j1+gBLw==",
"dependencies": {
"express": "^4.17.1"
}
@ -27687,9 +27687,9 @@
}
},
"nc-lib-gui": {
"version": "0.104.2",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.104.2.tgz",
"integrity": "sha512-B4FmViGweFoOieL0mFXdiUWV4GtOQo7XCun2so2So78pJD49yIq6d3oxN8+xiNyGNTvsophcCU9xchV8NLK9/Q==",
"version": "0.104.3",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.104.3.tgz",
"integrity": "sha512-gQjq2Yg5vYaai/RiRDrdrWlw+qqglpha0tHuZQ/xGwSuAO9Gn/v62YeeI0DAw+wvzR8CL+p/B5CQcz1j1+gBLw==",
"requires": {
"express": "^4.17.1"
}

4
packages/nocodb/package.json

@ -1,6 +1,6 @@
{
"name": "nocodb",
"version": "0.104.2",
"version": "0.104.3",
"description": "NocoDB Backend",
"main": "dist/bundle.js",
"author": {
@ -105,7 +105,7 @@
"mysql2": "^2.2.5",
"nanoid": "^3.1.20",
"nc-help": "0.2.85",
"nc-lib-gui": "0.104.2",
"nc-lib-gui": "0.104.3",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",

4
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/CustomKnex.ts

@ -548,7 +548,9 @@ knex.QueryBuilder.extend(
knex.QueryBuilder.extend('concat', function (cn: any) {
switch (this?.client?.config?.client) {
case 'pg':
this.select(this.client.raw(`STRING_AGG(??::character varying , ',')`, [cn]));
this.select(
this.client.raw(`STRING_AGG(??::character varying , ',')`, [cn])
);
break;
case 'mysql':
case 'mysql2':

138
packages/nocodb/tests/unit/rest/tests/attachment.test.ts

@ -1,143 +1,140 @@
import { expect } from 'chai'
import fs from 'fs'
import { OrgUserRoles, ProjectRoles } from 'nocodb-sdk'
import path from 'path'
import 'mocha'
import request from 'supertest'
import { createProject } from '../../factory/project'
import init from '../../init'
const FILE_PATH = path.join(__dirname, 'test.txt')
import { expect } from 'chai';
import fs from 'fs';
import { OrgUserRoles, ProjectRoles } from 'nocodb-sdk';
import path from 'path';
import 'mocha';
import request from 'supertest';
import { createProject } from '../../factory/project';
import init from '../../init';
const FILE_PATH = path.join(__dirname, 'test.txt');
// Test case list
// 1. Upload file - Super admin
// 2. Upload file - Without token
// 3. Upload file - Org level viewer
// 4. Upload file - Org level creator
// 5. Upload file - Org level viewer with editor role in a project
function attachmentTests() {
let context
beforeEach(async function() {
context = await init()
fs.writeFileSync(FILE_PATH, 'test', `utf-8`)
context = await init()
})
let context;
afterEach(function() {
fs.unlinkSync(FILE_PATH)
})
beforeEach(async function () {
context = await init();
fs.writeFileSync(FILE_PATH, 'test', `utf-8`);
context = await init();
});
afterEach(function () {
fs.unlinkSync(FILE_PATH);
});
it('Upload file - Super admin', async () => {
const response = await request(context.app)
.post('/api/v1/db/storage/upload')
.attach('files', FILE_PATH)
.set('xc-auth', context.token)
.expect(200)
.expect(200);
const attachments = response.body
expect(attachments).to.be.an('array')
expect(attachments[0].title).to.be.eq(path.basename(FILE_PATH))
})
const attachments = response.body;
expect(attachments).to.be.an('array');
expect(attachments[0].title).to.be.eq(path.basename(FILE_PATH));
});
it('Upload file - Without token', async () => {
const response = await request(context.app)
.post('/api/v1/db/storage/upload')
.attach('files', FILE_PATH)
.expect(401)
.expect(401);
const msg = response.body.msg
expect(msg).to.be.eq('Unauthorized')
})
const msg = response.body.msg;
expect(msg).to.be.eq('Unauthorized');
});
it('Upload file - Org level viewer', async () => {
// signup a user
const args = {
email: 'dummyuser@example.com',
password: 'A1234abh2@dsad',
}
};
const signupResponse = await request(context.app)
.post('/api/v1/auth/user/signup')
.send(args)
.expect(200)
.expect(200);
const response = await request(context.app)
.post('/api/v1/db/storage/upload')
.attach('files', FILE_PATH)
.set('xc-auth', signupResponse.body.token)
.expect(400)
const msg = response.body.msg
expect(msg).to.be.eq('Upload not allowed')
})
.expect(400);
const msg = response.body.msg;
expect(msg).to.be.eq('Upload not allowed');
});
it('Upload file - Org level creator', async () => {
// signup a user
const args = {
email: 'dummyuser@example.com',
password: 'A1234abh2@dsad',
}
};
await request(context.app)
.post('/api/v1/auth/user/signup')
.send(args)
.expect(200)
.expect(200);
// update user role to creator
const usersListResponse = await request(context.app)
.get('/api/v1/users')
.set('xc-auth', context.token)
.expect(200)
.expect(200);
const user = usersListResponse.body.list.find(u => u.email === args.email)
expect(user).to.have.property('roles').to.be.equal(OrgUserRoles.VIEWER)
const user = usersListResponse.body.list.find(
(u) => u.email === args.email
);
expect(user).to.have.property('roles').to.be.equal(OrgUserRoles.VIEWER);
await request(context.app)
.patch('/api/v1/users/' + user.id)
.set('xc-auth', context.token)
.send({ roles: OrgUserRoles.CREATOR })
.expect(200)
.expect(200);
const signinResponse = await request(context.app)
.post('/api/v1/auth/user/signin')
// pass empty data in await request
.send(args)
.expect(200)
.expect(200);
const response = await request(context.app)
.post('/api/v1/db/storage/upload')
.attach('files', FILE_PATH)
.set('xc-auth', signinResponse.body.token)
.expect(200)
const attachments = response.body
expect(attachments).to.be.an('array')
expect(attachments[0].title).to.be.eq(path.basename(FILE_PATH))
})
.expect(200);
const attachments = response.body;
expect(attachments).to.be.an('array');
expect(attachments[0].title).to.be.eq(path.basename(FILE_PATH));
});
it('Upload file - Org level viewer with editor role in a project', async () => {
// signup a new user
const args = {
email: 'dummyuser@example.com',
password: 'A1234abh2@dsad',
}
};
await request(context.app)
.post('/api/v1/auth/user/signup')
.send(args)
.expect(200)
.expect(200);
const newProject = await createProject(context, {
title: 'NewTitle1',
})
});
// invite user to project with editor role
await request(context.app)
@ -149,28 +146,27 @@ function attachmentTests() {
project_id: newProject.id,
projectName: newProject.title,
})
.expect(200)
.expect(200);
// signin to get user token
const signinResponse = await request(context.app)
.post('/api/v1/auth/user/signin')
// pass empty data in await request
.send(args)
.expect(200)
.expect(200);
const response = await request(context.app)
.post('/api/v1/db/storage/upload')
.attach('files', FILE_PATH)
.set('xc-auth', signinResponse.body.token)
.expect(200)
const attachments = response.body
expect(attachments).to.be.an('array')
expect(attachments[0].title).to.be.eq(path.basename(FILE_PATH))
})
.expect(200);
const attachments = response.body;
expect(attachments).to.be.an('array');
expect(attachments[0].title).to.be.eq(path.basename(FILE_PATH));
});
}
export default function() {
describe('Attachment', attachmentTests)
export default function () {
describe('Attachment', attachmentTests);
}

33
packages/nocodb/tests/unit/rest/tests/auth.test.ts

@ -4,6 +4,27 @@ import request from 'supertest';
import init from '../../init';
import { defaultUserArgs } from '../../factory/user';
// Test case list
// 1. Signup with valid email
// 2. Signup with invalid email
// 3. Signup with invalid password
// 4. Signin with valid credentials
// 5. Signin without email and password
// 6. Signin with invalid credentials
// 7. Signin with invalid password
// 8. me without token
// 9. me with token
// 10. forgot password with non-existing email id
// 11. TBD: forgot password with existing email id
// 12. Change password
// 13. Change password - after logout
// 14. TBD: Reset Password with an invalid token
// 15. TBD: Email validate with an invalid token
// 16. TBD: Email validate with a valid token
// 17. TBD: Forgot password validate with a valid token
// 18. TBD: Reset Password with an valid token
// 19. TBD: refresh token api
function authTests() {
let context;
@ -15,7 +36,7 @@ function authTests() {
const response = await request(context.app)
.post('/api/v1/auth/user/signup')
.send({ email: 'new@example.com', password: defaultUserArgs.password })
.expect(200)
.expect(200);
const token = response.body.token;
expect(token).to.be.a('string');
@ -43,8 +64,8 @@ function authTests() {
password: defaultUserArgs.password,
})
.expect(200);
const token = response.body.token;
expect(token).to.be.a('string');
const token = response.body.token;
expect(token).to.be.a('string');
});
it('Signup without email and password', async () => {
@ -75,9 +96,9 @@ function authTests() {
.unset('xc-auth')
.expect(200);
if (!response.body?.roles?.guest) {
return new Error('User should be guest');
}
if (!response.body?.roles?.guest) {
return new Error('User should be guest');
}
});
it('me with token', async () => {

6
packages/nocodb/tests/unit/rest/tests/columnTypeSpecific.test.ts

@ -11,6 +11,12 @@ import { expect } from 'chai';
import Column from '../../../../src/lib/models/Column';
import { title } from 'process';
// Test case list
// 1. Qr Code Column
// a. adding a QR code column which references another column
// - delivers the same cell values as the referenced column
// - gets deleted if the referenced column gets deleted
function columnTypeSpecificTests() {
let context;
let project: Project;

201
packages/nocodb/tests/unit/rest/tests/org.test.ts

@ -1,170 +1,174 @@
import { expect } from 'chai'
import 'mocha'
import request from 'supertest'
import { OrgUserRoles } from 'nocodb-sdk'
import init from '../../init'
import { expect } from 'chai';
import 'mocha';
import request from 'supertest';
import { OrgUserRoles } from 'nocodb-sdk';
import init from '../../init';
// Test case list in this file
// 1. Get users list
// 2. Invite a new user
// 3. Update user role
// 4. Remove user
// 5. Get token list
// 6. Generate token
// 7. Delete token
// 8. Disable/Enable signup
function authTests() {
let context
let context;
beforeEach(async function() {
context = await init()
})
beforeEach(async function () {
context = await init();
});
it('Get users list', async () => {
const response = await request(context.app)
.get('/api/v1/users')
.set('xc-auth', context.token)
.expect(200)
.expect(200);
expect(response.body).to.have.keys(['list', 'pageInfo'])
expect(response.body.list).to.have.length(1)
})
expect(response.body).to.have.keys(['list', 'pageInfo']);
expect(response.body.list).to.have.length(1);
});
it('Invite a new user', async () => {
const response = await request(context.app)
.post('/api/v1/users')
.set('xc-auth', context.token).send({ email: 'a@nocodb.com' })
.expect(200)
.set('xc-auth', context.token)
.send({ email: 'a@nocodb.com' })
.expect(200);
console.log(response.body)
console.log(response.body);
expect(response.body).to.have.property('invite_token').to.be.a('string')
// todo: verify invite token
})
expect(response.body).to.have.property('invite_token').to.be.a('string');
// todo: verify invite token
});
it('Update user role', async () => {
const email = 'a@nocodb.com'
const email = 'a@nocodb.com';
// invite a user
await request(context.app)
.post('/api/v1/users')
.set('xc-auth', context.token).send({ email })
.expect(200)
.set('xc-auth', context.token)
.send({ email })
.expect(200);
const response = await request(context.app)
.get('/api/v1/users')
.set('xc-auth', context.token)
.expect(200)
expect(response.body.list).to.have.length(2)
.expect(200);
expect(response.body.list).to.have.length(2);
const user = response.body.list.find(u => u.email === email)
expect(user).to.have.property('roles').to.be.equal(OrgUserRoles.VIEWER)
const user = response.body.list.find((u) => u.email === email);
expect(user).to.have.property('roles').to.be.equal(OrgUserRoles.VIEWER);
await request(context.app)
.patch('/api/v1/users/' + user.id)
.set('xc-auth', context.token)
.send({ roles: OrgUserRoles.CREATOR })
.expect(200)
.expect(200);
const response2 = await request(context.app)
.get('/api/v1/users')
.set('xc-auth', context.token)
.expect(200)
expect(response2.body.list).to.have.length(2)
.expect(200);
expect(response2.body.list).to.have.length(2);
const user2 = response2.body.list.find(u => u.email === email)
const user2 = response2.body.list.find((u) => u.email === email);
expect(user2).to.have.property('roles').to.be.equal(OrgUserRoles.CREATOR)
})
expect(user2).to.have.property('roles').to.be.equal(OrgUserRoles.CREATOR);
});
it('Remove user', async () => {
const email = 'a@nocodb.com'
const email = 'a@nocodb.com';
// invite a user
await request(context.app)
.post('/api/v1/users')
.set('xc-auth', context.token).send({ email })
.expect(200)
.set('xc-auth', context.token)
.send({ email })
.expect(200);
const response = await request(context.app)
.get('/api/v1/users')
.set('xc-auth', context.token)
.expect(200)
expect(response.body.list).to.have.length(2)
.expect(200);
expect(response.body.list).to.have.length(2);
const user = response.body.list.find(u => u.email === email)
expect(user).to.have.property('roles').to.be.equal(OrgUserRoles.VIEWER)
const user = response.body.list.find((u) => u.email === email);
expect(user).to.have.property('roles').to.be.equal(OrgUserRoles.VIEWER);
await request(context.app)
.delete('/api/v1/users/' + user.id)
.set('xc-auth', context.token)
.expect(200)
.expect(200);
const response2 = await request(context.app)
.get('/api/v1/users')
.set('xc-auth', context.token)
.expect(200)
expect(response2.body.list).to.have.length(1)
})
.expect(200);
expect(response2.body.list).to.have.length(1);
});
it('Get token list', async () => {
const response = await request(context.app)
.get('/api/v1/tokens')
.set('xc-auth', context.token)
.expect(200)
expect(response.body).to.have.keys(['list', 'pageInfo'])
expect(response.body.list).to.have.length(0)
.expect(200);
})
expect(response.body).to.have.keys(['list', 'pageInfo']);
expect(response.body.list).to.have.length(0);
});
it('Generate token', async () => {
const r = await request(context.app)
.post('/api/v1/tokens')
.set('xc-auth', context.token)
.send({ description: 'test' })
.expect(200)
.expect(200);
const response = await request(context.app)
.get('/api/v1/tokens')
.set('xc-auth', context.token)
.expect(200)
expect(response.body).to.have.keys(['list', 'pageInfo'])
expect(response.body.list).to.have.length(1)
expect(response.body.list[0]).to.have.property('token').to.be.a('string')
expect(response.body.list[0]).to.have.property('description').to.be.a('string').to.be.eq('test')
.expect(200);
})
expect(response.body).to.have.keys(['list', 'pageInfo']);
expect(response.body.list).to.have.length(1);
expect(response.body.list[0]).to.have.property('token').to.be.a('string');
expect(response.body.list[0])
.to.have.property('description')
.to.be.a('string')
.to.be.eq('test');
});
it('Delete token', async () => {
const r = await request(context.app)
.post('/api/v1/tokens')
.set('xc-auth', context.token)
.send({ description: 'test' })
.expect(200)
.expect(200);
let response = await request(context.app)
.get('/api/v1/tokens')
.set('xc-auth', context.token)
.expect(200)
.expect(200);
expect(response.body).to.have.keys(['list', 'pageInfo'])
expect(response.body.list).to.have.length(1)
expect(response.body).to.have.keys(['list', 'pageInfo']);
expect(response.body.list).to.have.length(1);
await request(context.app)
.delete('/api/v1/tokens/' + r.body.token)
.set('xc-auth', context.token)
.expect(200)
.expect(200);
response = await request(context.app)
.get('/api/v1/tokens')
.set('xc-auth', context.token)
.expect(200)
expect(response.body).to.have.keys(['list', 'pageInfo'])
expect(response.body.list).to.have.length(0)
.expect(200);
})
expect(response.body).to.have.keys(['list', 'pageInfo']);
expect(response.body.list).to.have.length(0);
});
it.only('Disable/Enable signup', async () => {
const args = {
@ -172,51 +176,48 @@ function authTests() {
password: 'A1234abh2@dsad',
};
await request(context.app)
.post('/api/v1/app-settings')
.set('xc-auth', context.token)
.send({ invite_only_signup: true })
.expect(200)
.expect(200);
const failedRes = await request(context.app)
.post('/api/v1/auth/user/signup')
.send(args)
.expect(400)
const failedRes = await request(context.app)
.post('/api/v1/auth/user/signup')
.send(args)
.expect(400);
expect(failedRes.body).to.be.an('object')
.to.have.property('msg')
.to.be.equal('Not allowed to signup, contact super admin.')
expect(failedRes.body)
.to.be.an('object')
.to.have.property('msg')
.to.be.equal('Not allowed to signup, contact super admin.');
await request(context.app)
.post('/api/v1/app-settings')
.set('xc-auth', context.token)
.send({ invite_only_signup: false })
.expect(200)
.expect(200);
const successRes = await request(context.app)
const successRes = await request(context.app)
.post('/api/v1/auth/user/signup')
.send(args)
.expect(200)
.expect(200);
expect(successRes.body).to.be.an('object')
expect(successRes.body)
.to.be.an('object')
.to.have.property('token')
.to.be.a('string')
.to.be.a('string');
const userMeRes = await request(context.app)
const userMeRes = await request(context.app)
.get('/api/v1/auth/user/me')
.set('xc-auth', successRes.body.token)
.expect(200)
.expect(200);
expect(userMeRes.body).to.be.an('object')
expect(userMeRes.body)
.to.be.an('object')
.to.have.property('email')
.to.be.eq(args.email)
})
.to.be.eq(args.email);
});
}
export default function() {
}
export default function () {}

251
packages/nocodb/tests/unit/rest/tests/project.test.ts

@ -1,30 +1,48 @@
import 'mocha'
import request from 'supertest'
import { createTable } from '../../factory/table'
import init from '../../init/index'
import { createProject, createSharedBase } from '../../factory/project'
import { beforeEach } from 'mocha'
import { Exception } from 'handlebars'
import Project from '../../../../src/lib/models/Project'
import { expect } from 'chai'
import 'mocha';
import request from 'supertest';
import { createTable } from '../../factory/table';
import init from '../../init/index';
import { createProject, createSharedBase } from '../../factory/project';
import { beforeEach } from 'mocha';
import { Exception } from 'handlebars';
import Project from '../../../../src/lib/models/Project';
import { expect } from 'chai';
// Test case list
// 1. Get project info
// 2. UI ACL
// 3. Create project
// 4. Create project with existing title
// 5. Update project
// 6. Update project with existing title
// 7. Create project shared base
// 8. Created project shared base should have only editor or viewer role
// 9. Updated project shared base should have only editor or viewer role
// 10. Updated project shared base
// 11. Get project shared base
// 12. Delete project shared base
// 13. Meta diff sync
// 14. Meta diff sync
// 15. Meta diff sync
// 16. Get all projects meta
function projectTest() {
let context
let project
let context;
let project;
beforeEach(async function() {
context = await init()
beforeEach(async function () {
context = await init();
project = await createProject(context)
})
project = await createProject(context);
});
it('Get project info', async () => {
await request(context.app)
.get(`/api/v1/db/meta/projects/${project.id}/info`)
.set('xc-auth', context.token)
.send({})
.expect(200)
})
.expect(200);
});
// todo: Test by creating models under project and check if the UCL is working
it('UI ACL', async () => {
@ -32,8 +50,8 @@ function projectTest() {
.get(`/api/v1/db/meta/projects/${project.id}/visibility-rules`)
.set('xc-auth', context.token)
.send({})
.expect(200)
})
.expect(200);
});
// todo: Test creating visibility set
it('List projects', async () => {
@ -41,11 +59,12 @@ function projectTest() {
.get('/api/v1/db/meta/projects/')
.set('xc-auth', context.token)
.send({})
.expect(200)
.expect(200);
if (response.body.list.length !== 1) new Error('Should list only 1 project')
if (!response.body.pageInfo) new Error('Should have pagination info')
})
if (response.body.list.length !== 1)
new Error('Should list only 1 project');
if (!response.body.pageInfo) new Error('Should have pagination info');
});
it('Create project', async () => {
const response = await request(context.app)
@ -54,11 +73,11 @@ function projectTest() {
.send({
title: 'Title1',
})
.expect(200)
.expect(200);
const newProject = await Project.getByTitleOrId(response.body.id)
if (!newProject) return new Error('Project not created')
})
const newProject = await Project.getByTitleOrId(response.body.id);
if (!newProject) return new Error('Project not created');
});
it('Create projects with existing title', async () => {
await request(context.app)
@ -67,8 +86,8 @@ function projectTest() {
.send({
title: project.title,
})
.expect(400)
})
.expect(400);
});
// todo: fix passport user role popluation bug
// it('Delete project', async async () => {
@ -99,10 +118,11 @@ function projectTest() {
.get(`/api/v1/db/meta/projects/${project.id}`)
.set('xc-auth', context.token)
.send()
.expect(200)
.expect(200);
if (response.body.id !== project.id) return new Error('Got the wrong project')
})
if (response.body.id !== project.id)
return new Error('Got the wrong project');
});
it('Update projects', async () => {
await request(context.app)
@ -111,18 +131,18 @@ function projectTest() {
.send({
title: 'NewTitle',
})
.expect(200)
.expect(200);
const newProject = await Project.getByTitleOrId(project.id)
const newProject = await Project.getByTitleOrId(project.id);
if (newProject.title !== 'NewTitle') {
return new Error('Project not updated')
return new Error('Project not updated');
}
})
});
it('Update projects with existing title', async function() {
it('Update projects with existing title', async function () {
const newProject = await createProject(context, {
title: 'NewTitle1',
})
});
await request(context.app)
.patch(`/api/v1/db/meta/projects/${project.id}`)
@ -130,8 +150,8 @@ function projectTest() {
.send({
title: newProject.title,
})
.expect(400)
})
.expect(400);
});
it('Create project shared base', async () => {
await request(context.app)
@ -141,18 +161,18 @@ function projectTest() {
roles: 'viewer',
password: 'test',
})
.expect(200)
.expect(200);
const updatedProject = await Project.getByTitleOrId(project.id)
const updatedProject = await Project.getByTitleOrId(project.id);
if (
!updatedProject.uuid ||
updatedProject.roles !== 'viewer' ||
updatedProject.password !== 'test'
) {
return new Error('Shared base not configured properly')
return new Error('Shared base not configured properly');
}
})
});
it('Created project shared base should have only editor or viewer role', async () => {
await request(context.app)
@ -162,17 +182,17 @@ function projectTest() {
roles: 'commenter',
password: 'test',
})
.expect(200)
.expect(200);
const updatedProject = await Project.getByTitleOrId(project.id)
const updatedProject = await Project.getByTitleOrId(project.id);
if (updatedProject.roles === 'commenter') {
return new Error('Shared base not configured properly')
return new Error('Shared base not configured properly');
}
})
});
it('Updated project shared base should have only editor or viewer role', async () => {
await createSharedBase(context.app, context.token, project)
await createSharedBase(context.app, context.token, project);
await request(context.app)
.patch(`/api/v1/db/meta/projects/${project.id}/shared`)
@ -181,17 +201,17 @@ function projectTest() {
roles: 'commenter',
password: 'test',
})
.expect(200)
.expect(200);
const updatedProject = await Project.getByTitleOrId(project.id)
const updatedProject = await Project.getByTitleOrId(project.id);
if (updatedProject.roles === 'commenter') {
throw new Exception('Shared base not updated properly')
throw new Exception('Shared base not updated properly');
}
})
});
it('Updated project shared base', async () => {
await createSharedBase(context.app, context.token, project)
await createSharedBase(context.app, context.token, project);
await request(context.app)
.patch(`/api/v1/db/meta/projects/${project.id}/shared`)
@ -200,42 +220,42 @@ function projectTest() {
roles: 'editor',
password: 'test',
})
.expect(200)
const updatedProject = await Project.getByTitleOrId(project.id)
.expect(200);
const updatedProject = await Project.getByTitleOrId(project.id);
if (updatedProject.roles !== 'editor') {
throw new Exception('Shared base not updated properly')
throw new Exception('Shared base not updated properly');
}
})
});
it('Get project shared base', async () => {
await createSharedBase(context.app, context.token, project)
await createSharedBase(context.app, context.token, project);
await request(context.app)
.get(`/api/v1/db/meta/projects/${project.id}/shared`)
.set('xc-auth', context.token)
.send()
.expect(200)
.expect(200);
const updatedProject = await Project.getByTitleOrId(project.id)
const updatedProject = await Project.getByTitleOrId(project.id);
if (!updatedProject.uuid) {
throw new Exception('Shared base not created')
throw new Exception('Shared base not created');
}
})
});
it('Delete project shared base', async () => {
await createSharedBase(context.app, context.token, project)
await createSharedBase(context.app, context.token, project);
await request(context.app)
.delete(`/api/v1/db/meta/projects/${project.id}/shared`)
.set('xc-auth', context.token)
.send()
.expect(200)
const updatedProject = await Project.getByTitleOrId(project.id)
.expect(200);
const updatedProject = await Project.getByTitleOrId(project.id);
if (updatedProject.uuid) {
throw new Exception('Shared base not deleted')
throw new Exception('Shared base not deleted');
}
})
});
// todo: Do compare api test
@ -244,16 +264,16 @@ function projectTest() {
.get(`/api/v1/db/meta/projects/${project.id}/meta-diff`)
.set('xc-auth', context.token)
.send()
.expect(200)
})
.expect(200);
});
it('Meta diff sync', async () => {
await request(context.app)
.post(`/api/v1/db/meta/projects/${project.id}/meta-diff`)
.set('xc-auth', context.token)
.send()
.expect(200)
})
.expect(200);
});
// todo: improve test. Check whether the all the actions are present in the response and correct as well
it('Meta diff sync', async () => {
@ -261,45 +281,60 @@ function projectTest() {
.get(`/api/v1/db/meta/projects/${project.id}/audits`)
.set('xc-auth', context.token)
.send()
.expect(200)
})
.expect(200);
});
it('Get all projects meta', async () => {
await createTable(context, project, { table_name: 'table1', title: 'table1' })
await createTable(context, project, { table_name: 'table2', title: 'table2' })
await createTable(context, project, { table_name: 'table3', title: 'table3' })
await createTable(context, project, {
table_name: 'table1',
title: 'table1',
});
await createTable(context, project, {
table_name: 'table2',
title: 'table2',
});
await createTable(context, project, {
table_name: 'table3',
title: 'table3',
});
await request(context.app)
.get(`/api/v1/aggregated-meta-info`)
.set('xc-auth', context.token)
.send({})
.expect(200)
.then(res => {
.then((res) => {
expect(res.body).to.have.all.keys(
'userCount',
'sharedBaseCount',
'projectCount',
'projects',
)
expect(res.body).to.have.property('projectCount').to.eq(1)
expect(res.body).to.have.property('projects').to.be.an('array')
expect(res.body.projects[0].tableCount.table).to.be.eq(3)
expect(res.body).to.have.nested.property('projects[0].tableCount.table').to.be.a('number')
expect(res.body).to.have.nested.property('projects[0].tableCount.view').to.be.a('number')
expect(res.body).to.have.nested.property('projects[0].viewCount').to.be.an('object')
'projects'
);
expect(res.body).to.have.property('projectCount').to.eq(1);
expect(res.body).to.have.property('projects').to.be.an('array');
expect(res.body.projects[0].tableCount.table).to.be.eq(3);
expect(res.body)
.to.have.nested.property('projects[0].tableCount.table')
.to.be.a('number');
expect(res.body)
.to.have.nested.property('projects[0].tableCount.view')
.to.be.a('number');
expect(res.body)
.to.have.nested.property('projects[0].viewCount')
.to.be.an('object')
.have.keys(
'formCount',
'gridCount',
'galleryCount',
'kanbanCount',
'total',
'sharedFormCount',
'sharedGridCount',
'sharedGalleryCount',
'sharedKanbanCount',
'sharedTotal',
'sharedLockedCount')
'formCount',
'gridCount',
'galleryCount',
'kanbanCount',
'total',
'sharedFormCount',
'sharedGridCount',
'sharedGalleryCount',
'sharedKanbanCount',
'sharedTotal',
'sharedLockedCount'
);
expect(res.body.projects[0]).have.keys(
'external',
'webhookCount',
@ -308,14 +343,18 @@ function projectTest() {
'userCount',
'rowCount',
'tableCount',
'viewCount',
)
expect(res.body).to.have.nested.property('projects[0].rowCount').to.be.an('array')
expect(res.body).to.have.nested.property('projects[0].external').to.be.an('boolean')
})
})
'viewCount'
);
expect(res.body)
.to.have.nested.property('projects[0].rowCount')
.to.be.an('array');
expect(res.body)
.to.have.nested.property('projects[0].external')
.to.be.an('boolean');
});
});
}
export default function() {
describe('Project', projectTest)
export default function () {
describe('Project', projectTest);
}

12
packages/nocodb/tests/unit/rest/tests/table.test.ts

@ -7,6 +7,18 @@ import { defaultColumns } from '../../factory/column';
import Model from '../../../../src/lib/models/Model';
import { expect } from 'chai';
// Test case list
// 1. Get table list
// 2. Create table
// 3. Create table with same table name
// 4. Create table with same title
// 5. Create table with title length more than the limit
// 6. Create table with title having leading white space
// 7. Update table
// 8. Delete table
// 9. Get table
// 10. Reorder table
function tableTest() {
let context;
let project;

114
packages/nocodb/tests/unit/rest/tests/tableRow.test.ts

@ -34,6 +34,70 @@ const isColumnsCorrectInResponse = (row, columns: ColumnType[]) => {
return responseColumnsListStr === customerColumnsListStr;
};
// Test case list
// 1. Get table data list
// 2. Get table data list with required columns
// 3. Get desc sorted table data list with required columns
// 4. Get asc sorted table data list with required columns
// 5. Get sorted table data list with a rollup column
// 6. Get sorted table data list with a lookup column
// 7. Get filtered table data list with a lookup column
// 8. Get filtered table data list with a (hm)lookup column
// 9. Get nested sorted filtered table data list with a lookup column
// 10. Get nested sorted filtered table data list with a lookup column with date comparison
// 11. Get nested sorted filtered table data list with a rollup column in customer table
// 12. Get nested sorted filtered table with nested fields data list with a rollup column in customer table
// 13. Sorted Formula column on rollup customer table
// 14. Create table row
// 15. Create table row with wrong table id
// 16. Find one sorted table data list with required columns
// 17. Find one desc sorted and with rollup table data list with required columns
// 18. Find one sorted filtered table with nested fields data list with a rollup column in customer table
// 19. Groupby desc sorted and with rollup table data list with required columns
// 20. Groupby desc sorted and with rollup table data list with required columns
// 21. Read table row
// 22. Update table row
// 23. Update table row with validation and invalid data
// 24. Update table row with validation and valid data
// 25. Delete table row
// 26. Delete table row with foreign key contraint
// 27. Exist should be true table row when it exists
// 28. Exist should be false table row when it does not exists
// 29. Bulk insert
// 30. Bulk insert 400 records
// 31. Bulk update
// 32. Bulk delete
// 33. Export csv
// 34. Export excel
// 35. Nested row list hm
// 36. Nested row list hm with limit and offset
// 37. Row list hm with invalid table id
// 38. Nested row list mm
// 39. Nested row list mm with limit and offset
// 40. Row list mm with invalid table id
// 41. Create hm relation with invalid table id
// 42. Create hm relation with non ltar column
// 43. Create list hm wrong column id
// 44. Create list hm
// 45. Create list mm wrong column id
// 46. Create mm relation with non ltar column
// 47. Create list mm existing ref row id
// 48. Create list mm
// 49. List hm with non ltar column
// 50. List mm with non ltar column
// 51. Delete mm existing ref row id
// 52. Delete list hm with existing ref row id with non nullable clause
// 53. Delete list hm with existing ref row id
// 54. Exclude list hm
// 55. Exclude list hm with limit and offset
// 56. Exclude list mm
// 57. Exclude list mm with offset
// 58. Exclude list bt
// 59. Exclude list bt with offset
// 60. Create nested hm relation with invalid table id
// 61. Create nested mm relation with invalid table id
// 62. Get grouped data list
function tableTest() {
let context;
let project: Project;
@ -730,7 +794,7 @@ function tableTest() {
);
const nestedFields = {
'Rental List': { fields : ['RentalDate', 'ReturnDate'] },
'Rental List': { fields: ['RentalDate', 'ReturnDate'] },
};
const nestedFilter = [
@ -2218,39 +2282,49 @@ function tableTest() {
it('Exclude list bt', async () => {
const rowId = 1;
const addressTable = await getTable({project: sakilaProject, name: 'address'});
const addressTable = await getTable({
project: sakilaProject,
name: 'address',
});
const cityColumn = (await addressTable.getColumns()).find(
(column) => column.title === 'City'
)!;
const response = await request(context.app)
.get(`/api/v1/db/data/noco/${sakilaProject.id}/${addressTable.id}/${rowId}/bt/${cityColumn.id}/exclude`)
.set('xc-auth', context.token)
.expect(200);
.get(
`/api/v1/db/data/noco/${sakilaProject.id}/${addressTable.id}/${rowId}/bt/${cityColumn.id}/exclude`
)
.set('xc-auth', context.token)
.expect(200);
expect(response.body.pageInfo.totalRows).equal(599)
expect(response.body.list[0]['City']).equal('A Corua (La Corua)')
})
expect(response.body.pageInfo.totalRows).equal(599);
expect(response.body.list[0]['City']).equal('A Corua (La Corua)');
});
it('Exclude list bt with offset', async () => {
const rowId = 1;
const addressTable = await getTable({project: sakilaProject, name: 'address'});
const addressTable = await getTable({
project: sakilaProject,
name: 'address',
});
const cityColumn = (await addressTable.getColumns()).find(
(column) => column.title === 'City'
)!;
const response = await request(context.app)
.get(`/api/v1/db/data/noco/${sakilaProject.id}/${addressTable.id}/${rowId}/bt/${cityColumn.id}/exclude`)
.set('xc-auth', context.token)
.query({
limit: 40,
offset: 60
})
.expect(200);
expect(response.body.pageInfo.totalRows).equal(599)
expect(response.body.list[0]['City']).equal('Baybay')
})
.get(
`/api/v1/db/data/noco/${sakilaProject.id}/${addressTable.id}/${rowId}/bt/${cityColumn.id}/exclude`
)
.set('xc-auth', context.token)
.query({
limit: 40,
offset: 60,
})
.expect(200);
expect(response.body.pageInfo.totalRows).equal(599);
expect(response.body.list[0]['City']).equal('Baybay');
});
it('Create nested hm relation with invalid table id', async () => {
const rowId = 1;

70
packages/nocodb/tests/unit/rest/tests/viewRow.test.ts

@ -23,6 +23,74 @@ import {
} from '../../factory/row';
import { expect } from 'chai';
// Test case list
// 1. Get view row list g
// 2. Get view row list
// 3. Get view row lis
// 4. Get view row lis
// 5. Get view data list with required columns g
// 6. Get view data list with required column
// 7. Get view data list with required column
// 8. Get grouped view data list with required columns
// 9. Get desc sorted table data list with required columns gallery
// 10. Get desc sorted table data list with required columns form
// 11. Get desc sorted table data list with required columns grid
// 12. Get desc sorted table data list with required columns kanban
// 13. Get asc sorted view data list with required columns gallery
// 14. Get asc sorted view data list with required columns form
// 15. Get asc sorted view data list with required columns grid
// 16. Get asc sorted table data list with required columns kanban
// 17. Get nested sorted filtered table data list with a lookup column gallery
// 18. Get nested sorted filtered table data list with a lookup column grid
// 19. Get nested sorted filtered table with nested fields data list with a rollup column in customer table vie
// 20. Create table row grid
// 21. Create table row gallery
// 22. Create table row form
// 23. Create table row kanban
// 24. Create table row grid wrong grid id
// 25. Create table row wrong gallery id
// 26. Create table row wrong form id
// 27. Create table row wrong kanban id
// 28. Find one sorted data list with required columns gallery
// 29. Find one sorted data list with required columns form
// 30. Find one sorted data list with required columns grid
// 31. Find one view sorted filtered view with nested fields data list with a rollup column in customer table GRID
// 32. Groupby desc sorted and with rollup view data list with required columns GRID
// 33. Groupby desc sorted and with rollup view data list with required columns FORM
// 34. Groupby desc sorted and with rollup view data list with required columns GALLERY
// 35. Groupby desc sorted and with rollup view data list with required columns GALLERY
// 36. Groupby desc sorted and with rollup view data list with required columns FORM
// 37. Groupby desc sorted and with rollup view data list with required columns GRID
// 38. Count view data list with required columns GRID
// 39. Count view data list with required columns FORM
// 40. Count view data list with required columns GALLERY
// 41. Read view row GALLERY
// 42. Read view row FORM
// 43. Read view row GRID
// 44. Update view row GALLERY
// 45. Update view row GRID
// 46. Update view row FORM
// 47. Update view row with validation and invalid data GALLERY
// 48. Update view row with validation and invalid data GRID
// 49. Update view row with validation and invalid data FORM
// 50. Update view row with validation and valid data GALLERY
// 51. Update view row with validation and valid data GRID
// 52. Update view row with validation and valid data FORM
// 53. Delete view row GALLERY
// 54. Delete view row GRID
// 55. Delete view row FORM
// 56. Delete view row with ltar foreign key constraint GALLERY
// 57. Delete view row with ltar foreign key constraint GRID
// 58. Delete view row with ltar foreign key constraint FORM
// 59. Exist should be true view row when it exists GALLERY
// 60. Exist should be true view row when it exists GRID
// 61. Exist should be true view row when it exists FORM
// 62. Exist should be false view row when it does not exist GALLERY
// 63. Exist should be false view row when it does not exist GRID
// 64. Exist should be false view row when it does not exist FORM
// 65. Export csv GRID
// 66. Export excel GRID
const isColumnsCorrectInResponse = (row, columns: ColumnType[]) => {
const responseColumnsListStr = Object.keys(row).sort().join(',');
const customerColumnsListStr = columns
@ -562,7 +630,7 @@ function viewRowTests() {
);
const nestedFields = {
'Rental List': { fields : ['RentalDate', 'ReturnDate'] },
'Rental List': { fields: ['RentalDate', 'ReturnDate'] },
};
const nestedFilter = [

Loading…
Cancel
Save