forked from tsai/budibase
454 changed files with 11909 additions and 9306 deletions
@ -0,0 +1,62 @@ |
|||
name: Deploy Budibase Single Container Image to DockerHub |
|||
on: |
|||
push: |
|||
branches: |
|||
- "omnibus-action" |
|||
- "develop" |
|||
- "master" |
|||
- "main" |
|||
env: |
|||
BASE_BRANCH: ${{ github.event.pull_request.base.ref}} |
|||
BRANCH: ${{ github.event.pull_request.head.ref }} |
|||
CI: true |
|||
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }} |
|||
REGISTRY_URL: registry.hub.docker.com |
|||
jobs: |
|||
build: |
|||
name: "build" |
|||
runs-on: ubuntu-latest |
|||
strategy: |
|||
matrix: |
|||
node-version: [14.x] |
|||
steps: |
|||
- name: "Checkout" |
|||
uses: actions/checkout@v2 |
|||
- name: Use Node.js ${{ matrix.node-version }} |
|||
uses: actions/setup-node@v1 |
|||
with: |
|||
node-version: ${{ matrix.node-version }} |
|||
- name: Setup QEMU |
|||
uses: docker/setup-qemu-action@v1 |
|||
- name: Setup Docker Buildx |
|||
id: buildx |
|||
uses: docker/setup-buildx-action@v1 |
|||
- name: Install Pro |
|||
run: yarn install:pro $BRANCH $BASE_BRANCH |
|||
- name: Run Yarn |
|||
run: yarn |
|||
- name: Run Yarn Bootstrap |
|||
run: yarn bootstrap |
|||
- name: Runt Yarn Lint |
|||
run: yarn lint |
|||
- name: Run Yarn Build |
|||
run: yarn build |
|||
- name: Login to Docker Hub |
|||
uses: docker/login-action@v2 |
|||
with: |
|||
username: ${{ secrets.DOCKER_USERNAME }} |
|||
password: ${{ secrets.DOCKER_API_KEY }} |
|||
- name: Get the latest release version |
|||
id: version |
|||
run: | |
|||
release_version=$(cat lerna.json | jq -r '.version') |
|||
echo $release_version |
|||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV |
|||
- name: Tag and release Budibase service docker image |
|||
uses: docker/build-push-action@v2 |
|||
with: |
|||
context: . |
|||
push: true |
|||
platforms: linux/amd64,linux/arm64 |
|||
tags: budibase/budibase,budibase/budibase:v${{ env.RELEASE_VERSION }} |
|||
file: ./hosting/single/Dockerfile |
|||
@ -0,0 +1 @@ |
|||
network-timeout 100000 |
|||
@ -0,0 +1,17 @@ |
|||
#!/bin/bash |
|||
|
|||
echo ${TARGETBUILD} > /buildtarget.txt |
|||
if [[ "${TARGETBUILD}" = "aas" ]]; then |
|||
# Azure AppService uses /home for persisent data & SSH on port 2222 |
|||
mkdir -p /home/budibase/{minio,couchdb} |
|||
mkdir -p /home/budibase/couchdb/data |
|||
chown -R couchdb:couchdb /home/budibase/couchdb/ |
|||
apt update |
|||
apt-get install -y openssh-server |
|||
sed -i 's#dir=/opt/couchdb/data/search#dir=/home/budibase/couchdb/data/search#' /opt/clouseau/clouseau.ini |
|||
sed -i 's#/minio/minio server /minio &#/minio/minio server /home/budibase/minio &#' /runner.sh |
|||
sed -i 's#database_dir = ./data#database_dir = /home/budibase/couchdb/data#' /opt/couchdb/etc/default.ini |
|||
sed -i 's#view_index_dir = ./data#view_index_dir = /home/budibase/couchdb/data#' /opt/couchdb/etc/default.ini |
|||
sed -i "s/#Port 22/Port 2222/" /etc/ssh/sshd_config |
|||
/etc/init.d/ssh restart |
|||
fi |
|||
@ -0,0 +1,5 @@ |
|||
; CouchDB Configuration Settings |
|||
|
|||
[couchdb] |
|||
database_dir = /data/couch/dbs |
|||
view_index_dir = /data/couch/views |
|||
@ -1,125 +0,0 @@ |
|||
user www-data www-data; |
|||
error_log /var/log/nginx/error.log; |
|||
pid /var/run/nginx.pid; |
|||
worker_processes auto; |
|||
worker_rlimit_nofile 8192; |
|||
|
|||
events { |
|||
worker_connections 1024; |
|||
} |
|||
|
|||
http { |
|||
limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=20r/s; |
|||
proxy_set_header Host $host; |
|||
charset utf-8; |
|||
sendfile on; |
|||
tcp_nopush on; |
|||
tcp_nodelay on; |
|||
server_tokens off; |
|||
types_hash_max_size 2048; |
|||
|
|||
# buffering |
|||
client_header_buffer_size 1k; |
|||
client_max_body_size 20M; |
|||
ignore_invalid_headers off; |
|||
proxy_buffering off; |
|||
|
|||
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' |
|||
'$status $body_bytes_sent "$http_referer" ' |
|||
'"$http_user_agent" "$http_x_forwarded_for"'; |
|||
|
|||
map $http_upgrade $connection_upgrade { |
|||
default "upgrade"; |
|||
} |
|||
|
|||
server { |
|||
listen 80 default_server; |
|||
listen [::]:80 default_server; |
|||
server_name _; |
|||
client_max_body_size 1000m; |
|||
ignore_invalid_headers off; |
|||
proxy_buffering off; |
|||
# port_in_redirect off; |
|||
|
|||
location ^~ /.well-known/acme-challenge/ { |
|||
default_type "text/plain"; |
|||
root /var/www/html; |
|||
break; |
|||
} |
|||
location = /.well-known/acme-challenge/ { |
|||
return 404; |
|||
} |
|||
|
|||
location /app { |
|||
proxy_pass http://127.0.0.1:4001; |
|||
} |
|||
|
|||
location = / { |
|||
proxy_pass http://127.0.0.1:4001; |
|||
} |
|||
|
|||
location ~ ^/(builder|app_) { |
|||
proxy_http_version 1.1; |
|||
proxy_set_header Connection $connection_upgrade; |
|||
proxy_set_header Upgrade $http_upgrade; |
|||
proxy_set_header X-Real-IP $remote_addr; |
|||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
|||
proxy_pass http://127.0.0.1:4001; |
|||
} |
|||
|
|||
location ~ ^/api/(system|admin|global)/ { |
|||
proxy_pass http://127.0.0.1:4002; |
|||
} |
|||
|
|||
location /worker/ { |
|||
proxy_pass http://127.0.0.1:4002; |
|||
rewrite ^/worker/(.*)$ /$1 break; |
|||
} |
|||
|
|||
location /api/ { |
|||
# calls to the API are rate limited with bursting |
|||
limit_req zone=ratelimit burst=20 nodelay; |
|||
|
|||
# 120s timeout on API requests |
|||
proxy_read_timeout 120s; |
|||
proxy_connect_timeout 120s; |
|||
proxy_send_timeout 120s; |
|||
|
|||
proxy_http_version 1.1; |
|||
proxy_set_header Connection $connection_upgrade; |
|||
proxy_set_header Upgrade $http_upgrade; |
|||
proxy_set_header X-Real-IP $remote_addr; |
|||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
|||
|
|||
proxy_pass http://127.0.0.1:4001; |
|||
} |
|||
|
|||
location /db/ { |
|||
proxy_pass http://127.0.0.1:5984; |
|||
rewrite ^/db/(.*)$ /$1 break; |
|||
} |
|||
|
|||
location / { |
|||
proxy_set_header X-Real-IP $remote_addr; |
|||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
|||
proxy_set_header X-Forwarded-Proto $scheme; |
|||
|
|||
proxy_connect_timeout 300; |
|||
proxy_http_version 1.1; |
|||
proxy_set_header Connection ""; |
|||
chunked_transfer_encoding off; |
|||
proxy_pass http://127.0.0.1:9000; |
|||
} |
|||
|
|||
client_header_timeout 60; |
|||
client_body_timeout 60; |
|||
keepalive_timeout 60; |
|||
|
|||
# gzip |
|||
gzip on; |
|||
gzip_vary on; |
|||
gzip_proxied any; |
|||
gzip_comp_level 6; |
|||
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; |
|||
} |
|||
} |
|||
@ -0,0 +1,91 @@ |
|||
server { |
|||
listen 80 default_server; |
|||
listen [::]:80 default_server; |
|||
server_name _; |
|||
|
|||
client_max_body_size 1000m; |
|||
ignore_invalid_headers off; |
|||
proxy_buffering off; |
|||
# port_in_redirect off; |
|||
|
|||
location ^~ /.well-known/acme-challenge/ { |
|||
default_type "text/plain"; |
|||
root /var/www/html; |
|||
break; |
|||
} |
|||
location = /.well-known/acme-challenge/ { |
|||
return 404; |
|||
} |
|||
|
|||
location /app { |
|||
proxy_pass http://127.0.0.1:4001; |
|||
} |
|||
|
|||
location = / { |
|||
proxy_pass http://127.0.0.1:4001; |
|||
} |
|||
|
|||
location ~ ^/(builder|app_) { |
|||
proxy_http_version 1.1; |
|||
proxy_set_header Connection $connection_upgrade; |
|||
proxy_set_header Upgrade $http_upgrade; |
|||
proxy_set_header X-Real-IP $remote_addr; |
|||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
|||
proxy_pass http://127.0.0.1:4001; |
|||
} |
|||
|
|||
location ~ ^/api/(system|admin|global)/ { |
|||
proxy_pass http://127.0.0.1:4002; |
|||
} |
|||
|
|||
location /worker/ { |
|||
proxy_pass http://127.0.0.1:4002; |
|||
rewrite ^/worker/(.*)$ /$1 break; |
|||
} |
|||
|
|||
location /api/ { |
|||
# calls to the API are rate limited with bursting |
|||
limit_req zone=ratelimit burst=20 nodelay; |
|||
|
|||
# 120s timeout on API requests |
|||
proxy_read_timeout 120s; |
|||
proxy_connect_timeout 120s; |
|||
proxy_send_timeout 120s; |
|||
|
|||
proxy_http_version 1.1; |
|||
proxy_set_header Connection $connection_upgrade; |
|||
proxy_set_header Upgrade $http_upgrade; |
|||
proxy_set_header X-Real-IP $remote_addr; |
|||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
|||
|
|||
proxy_pass http://127.0.0.1:4001; |
|||
} |
|||
|
|||
location /db/ { |
|||
proxy_pass http://127.0.0.1:5984; |
|||
rewrite ^/db/(.*)$ /$1 break; |
|||
} |
|||
|
|||
location / { |
|||
proxy_set_header X-Real-IP $remote_addr; |
|||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
|||
proxy_set_header X-Forwarded-Proto $scheme; |
|||
|
|||
proxy_connect_timeout 300; |
|||
proxy_http_version 1.1; |
|||
proxy_set_header Connection ""; |
|||
chunked_transfer_encoding off; |
|||
proxy_pass http://127.0.0.1:9000; |
|||
} |
|||
|
|||
client_header_timeout 60; |
|||
client_body_timeout 60; |
|||
keepalive_timeout 60; |
|||
|
|||
# gzip |
|||
gzip on; |
|||
gzip_vary on; |
|||
gzip_proxied any; |
|||
gzip_comp_level 6; |
|||
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
user www-data www-data; |
|||
error_log /var/log/nginx/error.log; |
|||
pid /var/run/nginx.pid; |
|||
worker_processes auto; |
|||
worker_rlimit_nofile 8192; |
|||
|
|||
events { |
|||
worker_connections 1024; |
|||
} |
|||
|
|||
http { |
|||
limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=20r/s; |
|||
proxy_set_header Host $host; |
|||
charset utf-8; |
|||
sendfile on; |
|||
tcp_nopush on; |
|||
tcp_nodelay on; |
|||
server_tokens off; |
|||
types_hash_max_size 2048; |
|||
|
|||
# buffering |
|||
client_header_buffer_size 1k; |
|||
client_max_body_size 20M; |
|||
ignore_invalid_headers off; |
|||
proxy_buffering off; |
|||
|
|||
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' |
|||
'$status $body_bytes_sent "$http_referer" ' |
|||
'"$http_user_agent" "$http_x_forwarded_for"'; |
|||
|
|||
map $http_upgrade $connection_upgrade { |
|||
default "upgrade"; |
|||
} |
|||
|
|||
include /etc/nginx/sites-enabled/*; |
|||
|
|||
} |
|||
@ -1,4 +1,4 @@ |
|||
#!/bin/bash |
|||
id=$(docker run -t -d -p 10000:10000 budibase:latest) |
|||
id=$(docker run -t -d -p 8080:80 budibase:latest) |
|||
docker exec -it $id bash |
|||
docker kill $id |
|||
|
|||
@ -1,5 +1,5 @@ |
|||
module.exports = { |
|||
Client: require("./src/redis"), |
|||
utils: require("./src/redis/utils"), |
|||
clients: require("./src/redis/authRedis"), |
|||
clients: require("./src/redis/init"), |
|||
} |
|||
|
|||
@ -0,0 +1,92 @@ |
|||
import { getTenantId } from "../../context" |
|||
import redis from "../../redis/init" |
|||
import RedisWrapper from "../../redis" |
|||
|
|||
function generateTenantKey(key: string) { |
|||
const tenantId = getTenantId() |
|||
return `${key}:${tenantId}` |
|||
} |
|||
|
|||
export = class BaseCache { |
|||
client: RedisWrapper | undefined |
|||
|
|||
constructor(client: RedisWrapper | undefined = undefined) { |
|||
this.client = client |
|||
} |
|||
|
|||
async getClient() { |
|||
return !this.client ? await redis.getCacheClient() : this.client |
|||
} |
|||
|
|||
async keys(pattern: string) { |
|||
const client = await this.getClient() |
|||
return client.keys(pattern) |
|||
} |
|||
|
|||
/** |
|||
* Read only from the cache. |
|||
*/ |
|||
async get(key: string, opts = { useTenancy: true }) { |
|||
key = opts.useTenancy ? generateTenantKey(key) : key |
|||
const client = await this.getClient() |
|||
return client.get(key) |
|||
} |
|||
|
|||
/** |
|||
* Write to the cache. |
|||
*/ |
|||
async store( |
|||
key: string, |
|||
value: any, |
|||
ttl: number | null = null, |
|||
opts = { useTenancy: true } |
|||
) { |
|||
key = opts.useTenancy ? generateTenantKey(key) : key |
|||
const client = await this.getClient() |
|||
await client.store(key, value, ttl) |
|||
} |
|||
|
|||
/** |
|||
* Remove from cache. |
|||
*/ |
|||
async delete(key: string, opts = { useTenancy: true }) { |
|||
key = opts.useTenancy ? generateTenantKey(key) : key |
|||
const client = await this.getClient() |
|||
return client.delete(key) |
|||
} |
|||
|
|||
/** |
|||
* Read from the cache. Write to the cache if not exists. |
|||
*/ |
|||
async withCache( |
|||
key: string, |
|||
ttl: number, |
|||
fetchFn: any, |
|||
opts = { useTenancy: true } |
|||
) { |
|||
const cachedValue = await this.get(key, opts) |
|||
if (cachedValue) { |
|||
return cachedValue |
|||
} |
|||
|
|||
try { |
|||
const fetchedValue = await fetchFn() |
|||
|
|||
await this.store(key, fetchedValue, ttl, opts) |
|||
return fetchedValue |
|||
} catch (err) { |
|||
console.error("Error fetching before cache - ", err) |
|||
throw err |
|||
} |
|||
} |
|||
|
|||
async bustCache(key: string, opts = { client: null }) { |
|||
const client = await this.getClient() |
|||
try { |
|||
await client.delete(generateTenantKey(key)) |
|||
} catch (err) { |
|||
console.error("Error busting cache - ", err) |
|||
throw err |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
require("../../../tests/utilities/TestConfiguration") |
|||
const { Writethrough } = require("../writethrough") |
|||
const { dangerousGetDB } = require("../../db") |
|||
const tk = require("timekeeper") |
|||
|
|||
const START_DATE = Date.now() |
|||
tk.freeze(START_DATE) |
|||
|
|||
const DELAY = 5000 |
|||
|
|||
const db = dangerousGetDB("test") |
|||
const db2 = dangerousGetDB("test2") |
|||
const writethrough = new Writethrough(db, DELAY), writethrough2 = new Writethrough(db2, DELAY) |
|||
|
|||
describe("writethrough", () => { |
|||
describe("put", () => { |
|||
let first |
|||
it("should be able to store, will go to DB", async () => { |
|||
const response = await writethrough.put({ _id: "test", value: 1 }) |
|||
const output = await db.get(response.id) |
|||
first = output |
|||
expect(output.value).toBe(1) |
|||
}) |
|||
|
|||
it("second put shouldn't update DB", async () => { |
|||
const response = await writethrough.put({ ...first, value: 2 }) |
|||
const output = await db.get(response.id) |
|||
expect(first._rev).toBe(output._rev) |
|||
expect(output.value).toBe(1) |
|||
}) |
|||
|
|||
it("should put it again after delay period", async () => { |
|||
tk.freeze(START_DATE + DELAY + 1) |
|||
const response = await writethrough.put({ ...first, value: 3 }) |
|||
const output = await db.get(response.id) |
|||
expect(response.rev).not.toBe(first._rev) |
|||
expect(output.value).toBe(3) |
|||
}) |
|||
}) |
|||
|
|||
describe("get", () => { |
|||
it("should be able to retrieve", async () => { |
|||
const response = await writethrough.get("test") |
|||
expect(response.value).toBe(3) |
|||
}) |
|||
}) |
|||
|
|||
describe("same doc, different databases (tenancy)", () => { |
|||
it("should be able to two different databases", async () => { |
|||
const resp1 = await writethrough.put({ _id: "db1", value: "first" }) |
|||
const resp2 = await writethrough2.put({ _id: "db1", value: "second" }) |
|||
expect(resp1.rev).toBeDefined() |
|||
expect(resp2.rev).toBeDefined() |
|||
expect((await db.get("db1")).value).toBe("first") |
|||
expect((await db2.get("db1")).value).toBe("second") |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
@ -0,0 +1,120 @@ |
|||
import BaseCache from "./base" |
|||
import { getWritethroughClient } from "../redis/init" |
|||
|
|||
const DEFAULT_WRITE_RATE_MS = 10000 |
|||
let CACHE: BaseCache | null = null |
|||
|
|||
interface CacheItem { |
|||
doc: any |
|||
lastWrite: number |
|||
} |
|||
|
|||
async function getCache() { |
|||
if (!CACHE) { |
|||
const client = await getWritethroughClient() |
|||
CACHE = new BaseCache(client) |
|||
} |
|||
return CACHE |
|||
} |
|||
|
|||
function makeCacheKey(db: PouchDB.Database, key: string) { |
|||
return db.name + key |
|||
} |
|||
|
|||
function makeCacheItem(doc: any, lastWrite: number | null = null): CacheItem { |
|||
return { doc, lastWrite: lastWrite || Date.now() } |
|||
} |
|||
|
|||
export async function put( |
|||
db: PouchDB.Database, |
|||
doc: any, |
|||
writeRateMs: number = DEFAULT_WRITE_RATE_MS |
|||
) { |
|||
const cache = await getCache() |
|||
const key = doc._id |
|||
let cacheItem: CacheItem | undefined = await cache.get(makeCacheKey(db, key)) |
|||
const updateDb = !cacheItem || cacheItem.lastWrite < Date.now() - writeRateMs |
|||
let output = doc |
|||
if (updateDb) { |
|||
const writeDb = async (toWrite: any) => { |
|||
// doc should contain the _id and _rev
|
|||
const response = await db.put(toWrite) |
|||
output = { |
|||
...doc, |
|||
_id: response.id, |
|||
_rev: response.rev, |
|||
} |
|||
} |
|||
try { |
|||
await writeDb(doc) |
|||
} catch (err: any) { |
|||
if (err.status !== 409) { |
|||
throw err |
|||
} else { |
|||
// get the rev, update over it - this is risky, may change in future
|
|||
const readDoc = await db.get(doc._id) |
|||
doc._rev = readDoc._rev |
|||
await writeDb(doc) |
|||
} |
|||
} |
|||
} |
|||
// if we are updating the DB then need to set the lastWrite to now
|
|||
cacheItem = makeCacheItem(output, updateDb ? null : cacheItem?.lastWrite) |
|||
await cache.store(makeCacheKey(db, key), cacheItem) |
|||
return { ok: true, id: output._id, rev: output._rev } |
|||
} |
|||
|
|||
export async function get(db: PouchDB.Database, id: string): Promise<any> { |
|||
const cache = await getCache() |
|||
const cacheKey = makeCacheKey(db, id) |
|||
let cacheItem: CacheItem = await cache.get(cacheKey) |
|||
if (!cacheItem) { |
|||
const doc = await db.get(id) |
|||
cacheItem = makeCacheItem(doc) |
|||
await cache.store(cacheKey, cacheItem) |
|||
} |
|||
return cacheItem.doc |
|||
} |
|||
|
|||
export async function remove( |
|||
db: PouchDB.Database, |
|||
docOrId: any, |
|||
rev?: any |
|||
): Promise<void> { |
|||
const cache = await getCache() |
|||
if (!docOrId) { |
|||
throw new Error("No ID/Rev provided.") |
|||
} |
|||
const id = typeof docOrId === "string" ? docOrId : docOrId._id |
|||
rev = typeof docOrId === "string" ? rev : docOrId._rev |
|||
try { |
|||
await cache.delete(makeCacheKey(db, id)) |
|||
} finally { |
|||
await db.remove(id, rev) |
|||
} |
|||
} |
|||
|
|||
export class Writethrough { |
|||
db: PouchDB.Database |
|||
writeRateMs: number |
|||
|
|||
constructor( |
|||
db: PouchDB.Database, |
|||
writeRateMs: number = DEFAULT_WRITE_RATE_MS |
|||
) { |
|||
this.db = db |
|||
this.writeRateMs = writeRateMs |
|||
} |
|||
|
|||
async put(doc: any) { |
|||
return put(this.db, doc, this.writeRateMs) |
|||
} |
|||
|
|||
async get(id: string) { |
|||
return get(this.db, id) |
|||
} |
|||
|
|||
async remove(docOrId: any, rev?: any) { |
|||
return remove(this.db, docOrId, rev) |
|||
} |
|||
} |
|||
@ -1,41 +0,0 @@ |
|||
exports.SEPARATOR = "_" |
|||
|
|||
const PRE_APP = "app" |
|||
const PRE_DEV = "dev" |
|||
|
|||
exports.DocumentTypes = { |
|||
USER: "us", |
|||
WORKSPACE: "workspace", |
|||
CONFIG: "config", |
|||
TEMPLATE: "template", |
|||
APP: PRE_APP, |
|||
DEV: PRE_DEV, |
|||
APP_DEV: `${PRE_APP}${exports.SEPARATOR}${PRE_DEV}`, |
|||
APP_METADATA: `${PRE_APP}${exports.SEPARATOR}metadata`, |
|||
ROLE: "role", |
|||
MIGRATIONS: "migrations", |
|||
DEV_INFO: "devinfo", |
|||
} |
|||
|
|||
exports.StaticDatabases = { |
|||
GLOBAL: { |
|||
name: "global-db", |
|||
docs: { |
|||
apiKeys: "apikeys", |
|||
usageQuota: "usage_quota", |
|||
licenseInfo: "license_info", |
|||
}, |
|||
}, |
|||
// contains information about tenancy and so on
|
|||
PLATFORM_INFO: { |
|||
name: "global-info", |
|||
docs: { |
|||
tenants: "tenants", |
|||
install: "install", |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
exports.APP_PREFIX = exports.DocumentTypes.APP + exports.SEPARATOR |
|||
exports.APP_DEV = exports.APP_DEV_PREFIX = |
|||
exports.DocumentTypes.APP_DEV + exports.SEPARATOR |
|||
@ -0,0 +1,58 @@ |
|||
export const SEPARATOR = "_" |
|||
export const UNICODE_MAX = "\ufff0" |
|||
|
|||
/** |
|||
* Can be used to create a few different forms of querying a view. |
|||
*/ |
|||
export enum AutomationViewModes { |
|||
ALL = "all", |
|||
AUTOMATION = "automation", |
|||
STATUS = "status", |
|||
} |
|||
|
|||
export enum ViewNames { |
|||
USER_BY_EMAIL = "by_email", |
|||
BY_API_KEY = "by_api_key", |
|||
USER_BY_BUILDERS = "by_builders", |
|||
LINK = "by_link", |
|||
ROUTING = "screen_routes", |
|||
AUTOMATION_LOGS = "automation_logs", |
|||
} |
|||
|
|||
export enum DocumentTypes { |
|||
USER = "us", |
|||
WORKSPACE = "workspace", |
|||
CONFIG = "config", |
|||
TEMPLATE = "template", |
|||
APP = "app", |
|||
DEV = "dev", |
|||
APP_DEV = "app_dev", |
|||
APP_METADATA = "app_metadata", |
|||
ROLE = "role", |
|||
MIGRATIONS = "migrations", |
|||
DEV_INFO = "devinfo", |
|||
AUTOMATION_LOG = "log_au", |
|||
} |
|||
|
|||
export const StaticDatabases = { |
|||
GLOBAL: { |
|||
name: "global-db", |
|||
docs: { |
|||
apiKeys: "apikeys", |
|||
usageQuota: "usage_quota", |
|||
licenseInfo: "license_info", |
|||
}, |
|||
}, |
|||
// contains information about tenancy and so on
|
|||
PLATFORM_INFO: { |
|||
name: "global-info", |
|||
docs: { |
|||
tenants: "tenants", |
|||
install: "install", |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
export const APP_PREFIX = exports.DocumentTypes.APP + exports.SEPARATOR |
|||
export const APP_DEV = exports.DocumentTypes.APP_DEV + exports.SEPARATOR |
|||
export const APP_DEV_PREFIX = APP_DEV |
|||
@ -0,0 +1,62 @@ |
|||
require("../../../tests/utilities/TestConfiguration") |
|||
const getUrlInfo = require("../pouch").getUrlInfo |
|||
|
|||
describe("pouch", () => { |
|||
describe("Couch DB URL parsing", () => { |
|||
it("should handle a null Couch DB URL", () => { |
|||
const info = getUrlInfo(null) |
|||
expect(info.url).toBeUndefined() |
|||
expect(info.auth.username).toBeUndefined() |
|||
}) |
|||
it("should be able to parse a basic Couch DB URL", () => { |
|||
const info = getUrlInfo("http://host.com") |
|||
expect(info.url).toBe("http://host.com") |
|||
expect(info.auth.username).toBeUndefined() |
|||
}) |
|||
it("should be able to parse a Couch DB basic URL with HTTPS", () => { |
|||
const info = getUrlInfo("https://host.com") |
|||
expect(info.url).toBe("https://host.com") |
|||
expect(info.auth.username).toBeUndefined() |
|||
}) |
|||
it("should be able to parse a basic Couch DB URL with a custom port", () => { |
|||
const info = getUrlInfo("https://host.com:1234") |
|||
expect(info.url).toBe("https://host.com:1234") |
|||
expect(info.auth.username).toBeUndefined() |
|||
}) |
|||
it("should be able to parse a Couch DB URL with auth", () => { |
|||
const info = getUrlInfo("https://user:pass@host.com:1234") |
|||
expect(info.url).toBe("https://host.com:1234") |
|||
expect(info.auth.username).toBe("user") |
|||
expect(info.auth.password).toBe("pass") |
|||
}) |
|||
it("should be able to parse a Couch DB URL with auth and special chars", () => { |
|||
const info = getUrlInfo("https://user:s:p@s://@://:d@;][~s@host.com:1234") |
|||
expect(info.url).toBe("https://host.com:1234") |
|||
expect(info.auth.username).toBe("user") |
|||
expect(info.auth.password).toBe("s:p@s://@://:d@;][~s") |
|||
}) |
|||
it("should be able to parse a Couch DB URL without a protocol", () => { |
|||
const info = getUrlInfo("host.com:1234") |
|||
expect(info.url).toBe("http://host.com:1234") |
|||
expect(info.auth.username).toBeUndefined() |
|||
}) |
|||
it("should be able to parse a Couch DB URL with auth and without a protocol", () => { |
|||
const info = getUrlInfo("user:s:p@s://@://:d@;][~s@host.com:1234") |
|||
expect(info.url).toBe("http://host.com:1234") |
|||
expect(info.auth.username).toBe("user") |
|||
expect(info.auth.password).toBe("s:p@s://@://:d@;][~s") |
|||
}) |
|||
it("should be able to parse a Couch DB URL with only username auth", () => { |
|||
const info = getUrlInfo("https://user@host.com:1234") |
|||
expect(info.url).toBe("https://host.com:1234") |
|||
expect(info.auth.username).toBe("user") |
|||
expect(info.auth.password).toBeUndefined() |
|||
}) |
|||
it("should be able to parse a Couch DB URL with only username auth and without a protocol", () => { |
|||
const info = getUrlInfo("user@host.com:1234") |
|||
expect(info.url).toBe("http://host.com:1234") |
|||
expect(info.auth.username).toBe("user") |
|||
expect(info.auth.password).toBeUndefined() |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,86 @@ |
|||
<script> |
|||
import "@spectrum-css/slider/dist/index-vars.css" |
|||
import { createEventDispatcher } from "svelte" |
|||
|
|||
export let value = false |
|||
export let id = null |
|||
export let disabled = false |
|||
export let min = 0 |
|||
export let max = 100 |
|||
export let step = 1 |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
const onChange = event => { |
|||
dispatch("change", event.target.value) |
|||
} |
|||
</script> |
|||
|
|||
<div> |
|||
<input |
|||
type="range" |
|||
{min} |
|||
{max} |
|||
{step} |
|||
{value} |
|||
{disabled} |
|||
{id} |
|||
on:change={onChange} |
|||
/> |
|||
</div> |
|||
|
|||
<style> |
|||
div { |
|||
display: grid; |
|||
place-items: center; |
|||
} |
|||
|
|||
input { |
|||
width: 100%; |
|||
padding: 0; |
|||
margin: 0; |
|||
-webkit-appearance: none; |
|||
background: transparent; |
|||
} |
|||
input::-webkit-slider-thumb { |
|||
-webkit-appearance: none; |
|||
} |
|||
input:focus { |
|||
outline: none; |
|||
} |
|||
|
|||
input[type="range"]::-webkit-slider-thumb { |
|||
-webkit-appearance: none; |
|||
border: 2px solid var(--spectrum-global-color-gray-700); |
|||
height: 16px; |
|||
width: 16px; |
|||
border-radius: 50%; |
|||
background: var(--background); |
|||
cursor: pointer; |
|||
transition: background 130ms ease-out; |
|||
margin-top: -7px; |
|||
} |
|||
input[type="range"]::-moz-range-thumb { |
|||
border: 2px solid var(--spectrum-global-color-gray-700); |
|||
height: 12px; |
|||
width: 12px; |
|||
border-radius: 50%; |
|||
background: var(--background); |
|||
cursor: pointer; |
|||
transition: background 130ms ease-out; |
|||
} |
|||
|
|||
input[type="range"]::-webkit-slider-runnable-track { |
|||
width: 100%; |
|||
height: 2px; |
|||
cursor: pointer; |
|||
background: var(--spectrum-global-color-gray-300); |
|||
border-radius: 2px; |
|||
} |
|||
input[type="range"]::-moz-range-track { |
|||
width: 100%; |
|||
height: 2px; |
|||
cursor: pointer; |
|||
background: var(--spectrum-global-color-gray-300); |
|||
border-radius: 2px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,24 @@ |
|||
<script> |
|||
import Field from "./Field.svelte" |
|||
import Slider from "./Core/Slider.svelte" |
|||
import { createEventDispatcher } from "svelte" |
|||
|
|||
export let value = null |
|||
export let label = null |
|||
export let labelPosition = "above" |
|||
export let min = 0 |
|||
export let max = 100 |
|||
export let step = 1 |
|||
export let disabled = false |
|||
export let error = null |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
const onChange = e => { |
|||
value = e.detail |
|||
dispatch("change", e.detail) |
|||
} |
|||
</script> |
|||
|
|||
<Field {label} {labelPosition} {error}> |
|||
<Slider {disabled} {value} {min} {max} {step} on:change={onChange} /> |
|||
</Field> |
|||
@ -0,0 +1,14 @@ |
|||
<div class="icon-side-nav"> |
|||
<slot /> |
|||
</div> |
|||
|
|||
<style> |
|||
.icon-side-nav { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: center; |
|||
padding: var(--spacing-s); |
|||
gap: var(--spacing-xs); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,56 @@ |
|||
<script> |
|||
import Icon from "../Icon/Icon.svelte" |
|||
import Tooltip from "../Tooltip/Tooltip.svelte" |
|||
import { fade } from "svelte/transition" |
|||
|
|||
export let icon |
|||
export let active = false |
|||
export let tooltip |
|||
|
|||
let showTooltip = false |
|||
</script> |
|||
|
|||
<div |
|||
class="icon-side-nav-item" |
|||
class:active |
|||
on:mouseover={() => (showTooltip = true)} |
|||
on:focus={() => (showTooltip = true)} |
|||
on:mouseleave={() => (showTooltip = false)} |
|||
on:click |
|||
> |
|||
<Icon name={icon} hoverable /> |
|||
{#if tooltip && showTooltip} |
|||
<div class="tooltip" in:fade={{ duration: 130, delay: 250 }}> |
|||
<Tooltip textWrapping direction="right" text={tooltip} /> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
.icon-side-nav-item { |
|||
width: 36px; |
|||
height: 36px; |
|||
display: grid; |
|||
place-items: center; |
|||
border-radius: 4px; |
|||
position: relative; |
|||
cursor: pointer; |
|||
transition: background 130ms ease-out; |
|||
} |
|||
.icon-side-nav-item:hover :global(svg), |
|||
.active :global(svg) { |
|||
color: var(--spectrum-global-color-gray-900); |
|||
} |
|||
.active { |
|||
background: var(--spectrum-global-color-gray-300); |
|||
} |
|||
.tooltip { |
|||
position: absolute; |
|||
pointer-events: none; |
|||
left: calc(100% - 4px); |
|||
top: 50%; |
|||
white-space: nowrap; |
|||
transform: translateY(-50%); |
|||
z-index: 1; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,132 @@ |
|||
import filterTests from "../../support/filterTests" |
|||
const interact = require('../../support/interact') |
|||
|
|||
filterTests(["smoke", "all"], () => { |
|||
context("Account Portals", () => { |
|||
|
|||
const bbUserEmail = "bbuser@test.com" |
|||
|
|||
before(() => { |
|||
cy.login() |
|||
cy.deleteApp("Cypress Tests") |
|||
cy.createApp("Cypress Tests", false) |
|||
|
|||
// Create new user
|
|||
cy.wait(500) |
|||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000}) |
|||
cy.createUser(bbUserEmail) |
|||
cy.contains("bbuser").click() |
|||
cy.wait(500) |
|||
|
|||
// Reset password
|
|||
cy.get(".spectrum-ActionButton-label", { timeout: 2000 }).contains("Force password reset").click({ force: true }) |
|||
|
|||
cy.get(".spectrum-Dialog-grid") |
|||
.find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('pwd') |
|||
|
|||
cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").click({ force: true }) |
|||
|
|||
// Login as new user and set password
|
|||
cy.logOut() |
|||
cy.get('@pwd').then((pwd) => { |
|||
cy.login(bbUserEmail, pwd) |
|||
}) |
|||
|
|||
for (let i = 0; i < 2; i++) { |
|||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).eq(i).type("test") |
|||
} |
|||
cy.get(interact.SPECTRUM_BUTTON).contains("Reset your password").click({ force: true }) |
|||
cy.logoutNoAppGrid() |
|||
}) |
|||
|
|||
it("should verify Admin Portal", () => { |
|||
cy.login() |
|||
cy.contains("Users").click() |
|||
cy.contains("bbuser").click() |
|||
|
|||
// Enable Development & Administration access
|
|||
cy.wait(500) |
|||
for (let i = 4; i < 6; i++) { |
|||
cy.get(interact.FIELD).eq(i).within(() => { |
|||
cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true }) |
|||
cy.get(interact.SPECTRUM_SWITCH_INPUT).should('be.enabled') |
|||
}) |
|||
} |
|||
bbUserLogin() |
|||
|
|||
// Verify available options for Admin portal
|
|||
cy.get(".spectrum-SideNav") |
|||
.should('contain', 'Apps') |
|||
//.and('contain', 'Usage')
|
|||
.and('contain', 'Users') |
|||
.and('contain', 'Auth') |
|||
.and('contain', 'Email') |
|||
.and('contain', 'Organisation') |
|||
.and('contain', 'Theming') |
|||
.and('contain', 'Update') |
|||
//.and('contain', 'Upgrade')
|
|||
|
|||
cy.logOut() |
|||
}) |
|||
|
|||
it("should verify Development Portal", () => { |
|||
// Only Development access should be enabled
|
|||
cy.login() |
|||
cy.contains("Users").click() |
|||
cy.contains("bbuser").click() |
|||
cy.wait(500) |
|||
cy.get(interact.FIELD).eq(5).within(() => { |
|||
cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true }) |
|||
}) |
|||
|
|||
bbUserLogin() |
|||
|
|||
// Verify available options for Admin portal
|
|||
cy.get(interact.SPECTRUM_SIDENAV) |
|||
.should('contain', 'Apps') |
|||
//.and('contain', 'Usage')
|
|||
.and('not.contain', 'Users') |
|||
.and('not.contain', 'Auth') |
|||
.and('not.contain', 'Email') |
|||
.and('not.contain', 'Organisation') |
|||
.and('contain', 'Theming') |
|||
.and('not.contain', 'Update') |
|||
.and('not.contain', 'Upgrade') |
|||
|
|||
cy.logOut() |
|||
}) |
|||
|
|||
it("should verify Standard Portal", () => { |
|||
// Development access should be disabled (Admin access is already disabled)
|
|||
cy.login() |
|||
cy.contains("Users").click() |
|||
cy.contains("bbuser").click() |
|||
cy.wait(500) |
|||
cy.get(interact.FIELD).eq(4).within(() => { |
|||
cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true }) |
|||
}) |
|||
|
|||
bbUserLogin() |
|||
|
|||
// Verify Standard Portal
|
|||
cy.get(interact.SPECTRUM_SIDENAV).should('not.exist') // No config sections
|
|||
cy.get(interact.CREATE_APP_BUTTON).should('not.exist') // No create app button
|
|||
cy.get(".app").should('not.exist') // No apps -> no roles assigned to user
|
|||
cy.get(interact.CONTAINER).should('contain', bbUserEmail) // Message containing users email
|
|||
|
|||
cy.logoutNoAppGrid() |
|||
}) |
|||
|
|||
const bbUserLogin = () => { |
|||
// Login as bbuser
|
|||
cy.logOut() |
|||
cy.login(bbUserEmail, "test") |
|||
} |
|||
|
|||
after(() => { |
|||
cy.login() |
|||
// Delete BB user
|
|||
cy.deleteUser(bbUserEmail) |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,237 @@ |
|||
import filterTests from "../../support/filterTests" |
|||
const interact = require('../../support/interact') |
|||
|
|||
filterTests(["smoke", "all"], () => { |
|||
context("User Management", () => { |
|||
before(() => { |
|||
cy.login() |
|||
cy.deleteApp("Cypress Tests") |
|||
cy.createApp("Cypress Tests", false) |
|||
}) |
|||
|
|||
it("should create a user via basic onboarding", () => { |
|||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000}) |
|||
cy.createUser("bbuser@test.com") |
|||
cy.get(interact.SPECTRUM_TABLE).should("contain", "bbuser") |
|||
}) |
|||
|
|||
it("should confirm basic permission for a New User", () => { |
|||
// Basic permission = development & administraton disabled
|
|||
cy.contains("bbuser").click() |
|||
// Confirm development and admin access are disabled
|
|||
for (let i = 4; i < 6; i++) { |
|||
cy.wait(500) |
|||
cy.get(interact.FIELD).eq(i).within(() => { |
|||
//cy.get(interact.SPECTRUM_SWITCH_INPUT).should('be.disabled')
|
|||
cy.get(".spectrum-Switch-switch").should('not.be.checked') |
|||
}) |
|||
} |
|||
// Existing apps appear within the No Access table
|
|||
cy.get(interact.SPECTRUM_TABLE, { timeout: 500 }).eq(1).should("not.contain", "No rows found") |
|||
// Configure roles table should not contain apps
|
|||
cy.get(interact.SPECTRUM_TABLE).eq(0).contains("No rows found") |
|||
}) |
|||
|
|||
if (Cypress.env("TEST_ENV")) { |
|||
it("should assign role types", () => { |
|||
// 3 apps minimum required - to assign an app to each role type
|
|||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) |
|||
.its("body") |
|||
.then(val => { |
|||
if (val.length < 3) { |
|||
for (let i = 1; i < 3; i++) { |
|||
const uuid = () => Cypress._.random(0, 1e6) |
|||
const name = uuid() |
|||
if(i < 1){ |
|||
cy.createApp(name, false) |
|||
} else { |
|||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000}) |
|||
cy.wait(1000) |
|||
cy.get(interact.CREATE_APP_BUTTON, { timeout: 2000 }).click({ force: true }) |
|||
cy.createAppFromScratch(name) |
|||
} |
|||
} |
|||
} |
|||
}) |
|||
// Navigate back to the user
|
|||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000}) |
|||
cy.get(interact.SPECTRUM_SIDENAV).contains("Users").click() |
|||
cy.get(interact.SPECTRUM_TABLE, { timeout: 1000 }).contains("bbuser").click() |
|||
for (let i = 0; i < 3; i++) { |
|||
cy.get(interact.SPECTRUM_TABLE, { timeout: 3000}) |
|||
.eq(1) |
|||
.find(interact.SPECTRUM_TABLE_ROW) |
|||
.eq(0) |
|||
.find(interact.SPECTRUM_TABLE_CELL) |
|||
.eq(0) |
|||
.click() |
|||
cy.get(interact.SPECTRUM_DIALOG_GRID, { timeout: 1000 }) |
|||
.contains("Choose an option") |
|||
.click() |
|||
.then(() => { |
|||
if (i == 0) { |
|||
cy.get(interact.SPECTRUM_MENU, { timeout: 2000 }).contains("Admin").click({ force: true }) |
|||
} |
|||
else if (i == 1) { |
|||
cy.get(interact.SPECTRUM_MENU, { timeout: 2000 }).contains("Power").click({ force: true }) |
|||
} |
|||
else if (i == 2) { |
|||
cy.get(interact.SPECTRUM_MENU, { timeout: 2000 }).contains("Basic").click({ force: true }) |
|||
} |
|||
cy.get(interact.SPECTRUM_BUTTON, { timeout: 2000 }) |
|||
.contains("Update role") |
|||
.click({ force: true }) |
|||
}) |
|||
cy.reload({ timeout: 5000 }) |
|||
cy.wait(1000) |
|||
} |
|||
// Confirm roles exist within Configure roles table
|
|||
cy.get(interact.SPECTRUM_TABLE, { timeout: 2000 }) |
|||
.eq(0) |
|||
.within(assginedRoles => { |
|||
expect(assginedRoles).to.contain("Admin") |
|||
expect(assginedRoles).to.contain("Power") |
|||
expect(assginedRoles).to.contain("Basic") |
|||
}) |
|||
}) |
|||
|
|||
it("should unassign role types", () => { |
|||
// Set each app within Configure roles table to 'No Access'
|
|||
cy.get(interact.SPECTRUM_TABLE) |
|||
.eq(0) |
|||
.find(interact.SPECTRUM_TABLE_ROW) |
|||
.its("length") |
|||
.then(len => { |
|||
for (let i = 0; i < len; i++) { |
|||
cy.get(interact.SPECTRUM_TABLE) |
|||
.eq(0) |
|||
.find(interact.SPECTRUM_TABLE_ROW) |
|||
.eq(0) |
|||
.find(interact.SPECTRUM_TABLE_CELL) |
|||
.eq(0) |
|||
.click() |
|||
.then(() => { |
|||
cy.get(interact.SPECTRUM_PICKER).eq(1).click({ force: true }) |
|||
cy.get(interact.SPECTRUM_POPOVER, { timeout: 500 }).contains("No Access").click() |
|||
}) |
|||
cy.get(interact.SPECTRUM_BUTTON) |
|||
.contains("Update role") |
|||
.click({ force: true }) |
|||
} |
|||
}) |
|||
// Confirm Configure roles table no longer has any apps in it
|
|||
cy.get(interact.SPECTRUM_TABLE, { timeout: 1000 }).eq(0).contains("No rows found") |
|||
}) |
|||
} |
|||
|
|||
it("should enable Developer access and verify application access", () => { |
|||
// Enable Developer access
|
|||
cy.get(interact.FIELD) |
|||
.eq(4) |
|||
.within(() => { |
|||
cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true }) |
|||
}) |
|||
// No Access table should now be empty
|
|||
cy.get(interact.CONTAINER) |
|||
.contains("No Access") |
|||
.parent() |
|||
.within(() => { |
|||
cy.get(interact.SPECTRUM_TABLE).contains("No rows found") |
|||
}) |
|||
|
|||
// Each app within Configure roles should have Admin access
|
|||
cy.get(interact.SPECTRUM_TABLE) |
|||
.eq(0) |
|||
.find(interact.SPECTRUM_TABLE_ROW) |
|||
.its("length") |
|||
.then(len => { |
|||
for (let i = 0; i < len; i++) { |
|||
cy.get(interact.SPECTRUM_TABLE) |
|||
.eq(0) |
|||
.find(interact.SPECTRUM_TABLE_ROW) |
|||
.eq(i) |
|||
.contains("Admin") |
|||
cy.wait(500) |
|||
} |
|||
}) |
|||
}) |
|||
|
|||
it("should disable Developer access and verify application access", () => { |
|||
// Disable Developer access
|
|||
cy.get(interact.FIELD) |
|||
.eq(4) |
|||
.within(() => { |
|||
cy.get(".spectrum-Switch-input").click({ force: true }) |
|||
}) |
|||
// Configure roles table should now be empty
|
|||
cy.get(interact.CONTAINER) |
|||
.contains("Configure roles") |
|||
.parent() |
|||
.within(() => { |
|||
cy.get(interact.SPECTRUM_TABLE).contains("No rows found") |
|||
}) |
|||
}) |
|||
|
|||
it("Should edit user details within user details page", () => { |
|||
// Add First name
|
|||
cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => { |
|||
cy.wait(500) |
|||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).wait(500).clear().click().type("bb") |
|||
}) |
|||
// Add Last name
|
|||
cy.get(interact.FIELD, { timeout: 1000 }).eq(3).within(() => { |
|||
cy.wait(500) |
|||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).click().wait(500).clear().type("test") |
|||
}) |
|||
cy.get(interact.FIELD, { timeout: 1000 }).eq(0).click() |
|||
// Reload page
|
|||
cy.reload() |
|||
|
|||
// Confirm details have been saved
|
|||
cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => { |
|||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', "bb") |
|||
}) |
|||
cy.get(interact.FIELD, { timeout: 1000 }).eq(3).within(() => { |
|||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).should('have.value', "test") |
|||
}) |
|||
}) |
|||
|
|||
it("should reset the users password", () => { |
|||
cy.get(interact.REGENERATE, { timeout: 500 }).contains("Force password reset").click({ force: true }) |
|||
|
|||
// Reset password modal
|
|||
cy.get(interact.SPECTRUM_DIALOG_GRID) |
|||
.find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('pwd') |
|||
cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").click({ force: true }) |
|||
|
|||
// Logout, then login with new password
|
|||
cy.logOut() |
|||
cy.get('@pwd').then((pwd) => { |
|||
cy.login("bbuser@test.com", pwd) |
|||
}) |
|||
|
|||
// Reset password screen
|
|||
for (let i = 0; i < 2; i++) { |
|||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).eq(i).type("test") |
|||
} |
|||
cy.get(interact.SPECTRUM_BUTTON).contains("Reset your password").click({ force: true }) |
|||
|
|||
// Confirm user logged in afer password change
|
|||
cy.get(".avatar > .icon").click({ force: true }) |
|||
|
|||
cy.get(".spectrum-Menu-item").contains("Update user information").click({ force: true }) |
|||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT) |
|||
.eq(0) |
|||
.invoke('val').should('eq', 'bbuser@test.com') |
|||
|
|||
// Logout and login as previous user
|
|||
cy.logoutNoAppGrid() |
|||
cy.login() |
|||
}) |
|||
|
|||
it("should delete a user", () => { |
|||
cy.deleteUser("bbuser@test.com") |
|||
cy.get(interact.SPECTRUM_TABLE, { timeout: 4000 }).should("not.have.text", "bbuser") |
|||
}) |
|||
}) |
|||
}) |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue