diff --git a/.eslintrc.json b/.eslintrc.json index 8eccb147a5..87f8269c50 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -16,8 +16,7 @@ "dist", "public", "*.spec.js", - "bundle.js", - "coverage" + "bundle.js" ], "plugins": ["svelte3"], "extends": ["eslint:recommended"], diff --git a/packages/backend-core/__mocks__/node-fetch.ts b/packages/backend-core/__mocks__/node-fetch.ts deleted file mode 100644 index 4c7127ee48..0000000000 --- a/packages/backend-core/__mocks__/node-fetch.ts +++ /dev/null @@ -1 +0,0 @@ -jest.mock("node-fetch", () => jest.fn()) diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 63adcfc19c..0239acb9a1 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -56,7 +56,7 @@ "@types/chance": "1.1.3", "@types/ioredis": "4.28.0", "@types/jest": "27.5.1", - "@types/koa": "2.0.52", + "@types/koa": "2.13.4", "@types/lodash": "4.14.180", "@types/node": "14.18.20", "@types/node-fetch": "2.6.1", @@ -68,7 +68,7 @@ "chance": "1.1.3", "ioredis-mock": "5.8.0", "jest": "28.1.1", - "koa": "2.7.0", + "koa": "2.13.4", "nodemon": "2.0.16", "pouchdb-adapter-memory": "7.2.2", "timekeeper": "2.2.0", diff --git a/packages/backend-core/src/middleware/tests/matchers.spec.ts b/packages/backend-core/src/middleware/tests/matchers.spec.ts new file mode 100644 index 0000000000..c39bbb6dd3 --- /dev/null +++ b/packages/backend-core/src/middleware/tests/matchers.spec.ts @@ -0,0 +1,134 @@ +import * as matchers from "../matchers" +import { structures } from "../../../tests" + +describe("matchers", () => { + it("matches by path and method", () => { + const pattern = [ + { + route: "/api/tests", + method: "POST", + }, + ] + const ctx = structures.koa.newContext() + ctx.request.url = "/api/tests" + ctx.request.method = "POST" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(true) + }) + + it("wildcards path", () => { + const pattern = [ + { + route: "/api/tests", + method: "POST", + }, + ] + const ctx = structures.koa.newContext() + ctx.request.url = "/api/tests/id/something/else" + ctx.request.method = "POST" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(true) + }) + + it("doesn't wildcard path with strict", () => { + const pattern = [ + { + route: "/api/tests", + method: "POST", + strict: true, + }, + ] + const ctx = structures.koa.newContext() + ctx.request.url = "/api/tests/id/something/else" + ctx.request.method = "POST" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(false) + }) + + it("matches with param", () => { + const pattern = [ + { + route: "/api/tests/:testId", + method: "GET", + }, + ] + const ctx = structures.koa.newContext() + ctx.request.url = "/api/tests/id" + ctx.request.method = "GET" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(true) + }) + + // TODO: Support the below behaviour + // Strict does not work when a param is present + // it("matches with param with strict", () => { + // const pattern = [{ + // route: "/api/tests/:testId", + // method: "GET", + // strict: true + // }] + // const ctx = structures.koa.newContext() + // ctx.request.url = "/api/tests/id" + // ctx.request.method = "GET" + // + // const built = matchers.buildMatcherRegex(pattern) + // + // expect(!!matchers.matches(ctx, built)).toBe(true) + // }) + + it("doesn't match by path", () => { + const pattern = [ + { + route: "/api/tests", + method: "POST", + }, + ] + const ctx = structures.koa.newContext() + ctx.request.url = "/api/unknown" + ctx.request.method = "POST" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(false) + }) + + it("doesn't match by method", () => { + const pattern = [ + { + route: "/api/tests", + method: "POST", + }, + ] + const ctx = structures.koa.newContext() + ctx.request.url = "/api/tests" + ctx.request.method = "GET" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(false) + }) + + it("matches by path and wildcard method", () => { + const pattern = [ + { + route: "/api/tests", + method: "ALL", + }, + ] + const ctx = structures.koa.newContext() + ctx.request.url = "/api/tests" + ctx.request.method = "GET" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(true) + }) +}) diff --git a/packages/backend-core/src/tenancy/tenancy.ts b/packages/backend-core/src/tenancy/tenancy.ts index 99955ca321..3ac0f5c314 100644 --- a/packages/backend-core/src/tenancy/tenancy.ts +++ b/packages/backend-core/src/tenancy/tenancy.ts @@ -231,12 +231,18 @@ export const getTenantIDFromCtx = ( const match = ctx.matched.find( (m: any) => !!m.paramNames.find((p: any) => p.name === "tenantId") ) + + // get the raw path url - without any query params + const ctxUrl = ctx.originalUrl + let url + if (ctxUrl.includes("?")) { + url = ctxUrl.split("?")[0] + } else { + url = ctxUrl + } + if (match) { - const params = match.params( - ctx.originalUrl, - match.captures(ctx.originalUrl), - {} - ) + const params = match.params(url, match.captures(url), {}) if (params.tenantId) { return params.tenantId } diff --git a/packages/backend-core/src/utils.ts b/packages/backend-core/src/utils.ts index 215288b93c..3b9bd611d4 100644 --- a/packages/backend-core/src/utils.ts +++ b/packages/backend-core/src/utils.ts @@ -30,8 +30,8 @@ async function resolveAppUrl(ctx: BBContext) { let possibleAppUrl = `/${appUrl.toLowerCase()}` let tenantId = tenancy.getTenantId() - if (!env.SELF_HOSTED) { - // always use the tenant id from the subdomain in cloud + if (env.MULTI_TENANCY) { + // always use the tenant id from the subdomain in multi tenancy // this ensures the logged-in user tenant id doesn't overwrite // e.g. in the case of viewing a public app while already logged-in to another tenant tenantId = tenancy.getTenantIDFromCtx(ctx, { diff --git a/packages/backend-core/tests/utilities/mocks/fetch.ts b/packages/backend-core/tests/utilities/mocks/fetch.ts new file mode 100644 index 0000000000..573b47db9f --- /dev/null +++ b/packages/backend-core/tests/utilities/mocks/fetch.ts @@ -0,0 +1,4 @@ +const mockFetch = jest.fn() +jest.mock("node-fetch", () => mockFetch) + +export default mockFetch diff --git a/packages/backend-core/tests/utilities/mocks/index.ts b/packages/backend-core/tests/utilities/mocks/index.ts index 7031b225ec..e71c739e26 100644 --- a/packages/backend-core/tests/utilities/mocks/index.ts +++ b/packages/backend-core/tests/utilities/mocks/index.ts @@ -2,3 +2,4 @@ import "./posthog" import "./events" export * as accounts from "./accounts" export * as date from "./date" +export { default as fetch } from "./fetch" diff --git a/packages/backend-core/tests/utilities/structures/koa.ts b/packages/backend-core/tests/utilities/structures/koa.ts index 6f0f7866e6..7084c90360 100644 --- a/packages/backend-core/tests/utilities/structures/koa.ts +++ b/packages/backend-core/tests/utilities/structures/koa.ts @@ -1,5 +1,13 @@ import { createMockContext } from "@shopify/jest-koa-mocks" +import { BBContext } from "@budibase/types" -export const newContext = () => { - return createMockContext() +export const newContext = (): BBContext => { + const ctx = createMockContext() + return { + ...ctx, + request: { + ...ctx.request, + body: {}, + }, + } } diff --git a/packages/backend-core/yarn.lock b/packages/backend-core/yarn.lock index 77216a9069..fd48d574b7 100644 --- a/packages/backend-core/yarn.lock +++ b/packages/backend-core/yarn.lock @@ -1079,7 +1079,7 @@ dependencies: "@types/koa" "*" -"@types/koa@*": +"@types/koa@*", "@types/koa@2.13.4": version "2.13.4" resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.4.tgz#10620b3f24a8027ef5cbae88b393d1b31205726b" integrity sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw== @@ -1093,18 +1093,6 @@ "@types/koa-compose" "*" "@types/node" "*" -"@types/koa@2.0.52": - version "2.0.52" - resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.0.52.tgz#7dd11de4189ab339ad66c4ccad153716b14e525f" - integrity sha512-cp/GTOhOYwomlSKqEoG0kaVEVJEzP4ojYmfa7EKaGkmkkRwJ4B/1VBLbQZ49Z+WJNvzXejQB/9GIKqMo9XLgFQ== - dependencies: - "@types/accepts" "*" - "@types/cookies" "*" - "@types/http-assert" "*" - "@types/keygrip" "*" - "@types/koa-compose" "*" - "@types/node" "*" - "@types/lodash@4.14.180": version "4.14.180" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670" @@ -1478,11 +1466,6 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== -any-promise@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" - integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== - anymatch@^3.0.3, anymatch@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" @@ -2056,14 +2039,6 @@ convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: dependencies: safe-buffer "~5.1.1" -cookies@~0.7.1: - version "0.7.3" - resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.3.tgz#7912ce21fbf2e8c2da70cf1c3f351aecf59dadfa" - integrity sha512-+gixgxYSgQLTaTIilDHAdlNPZDENDQernEMiIcZpYYP14zgHsCt4Ce1FEjFtcp6GefhozebB6orvhAAWx/IS0A== - dependencies: - depd "~1.1.2" - keygrip "~1.0.3" - cookies@~0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90" @@ -2134,13 +2109,6 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@~3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - debuglog@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -2201,7 +2169,7 @@ denque@^1.1.0: resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== -depd@^1.1.0, depd@^1.1.2, depd@~1.1.2: +depd@^1.1.0, depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== @@ -2353,11 +2321,6 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -error-inject@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" - integrity sha512-JM8N6PytDbmIYm1IhPWlo8vr3NtfjhDY/1MhD/a5b/aad/USE8a0+NsqE9d5n+GVGmuNkPQWm4bFQWv18d8tMg== - escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -3602,11 +3565,6 @@ jws@^3.0.0, jws@^3.1.4, jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" -keygrip@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.3.tgz#399d709f0aed2bab0a059e0cdd3a5023a053e1dc" - integrity sha512-/PpesirAIfaklxUzp4Yb7xBper9MwP6hNRA6BGGUFCgbJ+BM5CKBtsoxinNXkLHAr+GXS1/lSlF2rP7cv5Fl+g== - keygrip@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226" @@ -3626,26 +3584,11 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -koa-compose@^3.0.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" - integrity sha512-8gen2cvKHIZ35eDEik5WOo8zbVp9t4cP8p4hW4uE55waxolLRexKKrqfCpwhGVppnB40jWeF8bZeTVg99eZgPw== - dependencies: - any-promise "^1.1.0" - koa-compose@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877" integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw== -koa-convert@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0" - integrity sha512-K9XqjmEDStGX09v3oxR7t5uPRy0jqJdvodHa6wxWTHrTfDq0WUNnYTOOUZN6g8OM8oZQXprQASbiIXG2Ez8ehA== - dependencies: - co "^4.6.0" - koa-compose "^3.0.0" - koa-convert@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-2.0.0.tgz#86a0c44d81d40551bae22fee6709904573eea4f5" @@ -3654,11 +3597,6 @@ koa-convert@^2.0.0: co "^4.6.0" koa-compose "^4.1.0" -koa-is-json@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14" - integrity sha512-+97CtHAlWDx0ndt0J8y3P12EWLwTLMXIfMnYDev3wOTwH/RpBGMlfn4bDXlMEg1u73K6XRE9BbUp+5ZAYoRYWw== - koa-passport@4.1.4: version "4.1.4" resolved "https://registry.yarnpkg.com/koa-passport/-/koa-passport-4.1.4.tgz#5f1665c1c2a37ace79af9f970b770885ca30ccfa" @@ -3666,37 +3604,7 @@ koa-passport@4.1.4: dependencies: passport "^0.4.0" -koa@2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/koa/-/koa-2.7.0.tgz#7e00843506942b9d82c6cc33749f657c6e5e7adf" - integrity sha512-7ojD05s2Q+hFudF8tDLZ1CpCdVZw8JQELWSkcfG9bdtoTDzMmkRF6BQBU7JzIzCCOY3xd3tftiy/loHBUYaY2Q== - dependencies: - accepts "^1.3.5" - cache-content-type "^1.0.0" - content-disposition "~0.5.2" - content-type "^1.0.4" - cookies "~0.7.1" - debug "~3.1.0" - delegates "^1.0.0" - depd "^1.1.2" - destroy "^1.0.4" - error-inject "^1.0.0" - escape-html "^1.0.3" - fresh "~0.5.2" - http-assert "^1.3.0" - http-errors "^1.6.3" - is-generator-function "^1.0.7" - koa-compose "^4.1.0" - koa-convert "^1.2.0" - koa-is-json "^1.0.0" - on-finished "^2.3.0" - only "~0.0.2" - parseurl "^1.3.2" - statuses "^1.5.0" - type-is "^1.6.16" - vary "^1.1.2" - -koa@^2.13.4: +koa@2.13.4, koa@^2.13.4: version "2.13.4" resolved "https://registry.yarnpkg.com/koa/-/koa-2.13.4.tgz#ee5b0cb39e0b8069c38d115139c774833d32462e" integrity sha512-43zkIKubNbnrULWlHdN5h1g3SEKXOEzoAlRsHOTFpnlDu8JlAOZSMJBLULusuXRequboiwJcj5vtYXKB3k7+2g== @@ -4079,11 +3987,6 @@ mkdirp@^1.0.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" diff --git a/packages/builder/src/pages/builder/portal/manage/auth/index.svelte b/packages/builder/src/pages/builder/portal/manage/auth/index.svelte index dab0bfdd90..116fdeff28 100644 --- a/packages/builder/src/pages/builder/portal/manage/auth/index.svelte +++ b/packages/builder/src/pages/builder/portal/manage/auth/index.svelte @@ -20,11 +20,12 @@ Toggle, Tag, Tags, + Icon, + Helpers, } from "@budibase/bbui" import { onMount } from "svelte" import { API } from "api" import { organisation, admin } from "stores/portal" - import { Helpers } from "@budibase/bbui" const ConfigTypes = { Google: "google", @@ -40,7 +41,9 @@ // Indicate to user that callback is based on platform url // If there is an existing value, indicate that it may be removed to return to default behaviour - $: googleCallbackTooltip = googleCallbackReadonly + $: googleCallbackTooltip = $admin.cloud + ? null + : googleCallbackReadonly ? "Vist the organisation page to update the platform URL" : "Leave blank to use the default callback URL" @@ -54,6 +57,7 @@ readonly: googleCallbackReadonly, tooltip: googleCallbackTooltip, placeholder: $organisation.googleCallbackUrl, + copyButton: true, }, ], } @@ -66,9 +70,12 @@ { name: "callbackURL", readonly: true, - tooltip: "Vist the organisation page to update the platform URL", + tooltip: $admin.cloud + ? null + : "Vist the organisation page to update the platform URL", label: "Callback URL", placeholder: $organisation.oidcCallbackUrl, + copyButton: true, }, ], } @@ -231,6 +238,11 @@ }, ] + const copyToClipboard = async value => { + await Helpers.copyToClipboard(value) + notifications.success("Copied") + } + onMount(async () => { try { await organisation.init() @@ -336,11 +348,23 @@ {#each GoogleConfigFields.Google as field}
- +
+
+ +
+ {#if field.copyButton} +
copyToClipboard(field.placeholder)} + > + +
+ {/if} +
{/each}
@@ -375,12 +399,23 @@ {#each OIDCConfigFields.Oidc as field}
- +
+
+ +
+ {#if field.copyButton} +
copyToClipboard(field.placeholder)} + > + +
+ {/if} +
{/each} @@ -557,4 +592,16 @@ .provider-title span { flex: 1 1 auto; } + .inputContainer { + display: flex; + flex-direction: row; + } + .input { + flex: 1; + } + .copy { + display: flex; + align-items: center; + margin-left: 10px; + } diff --git a/packages/types/src/sdk/auth.ts b/packages/types/src/sdk/auth.ts index d61b679c62..766d18a606 100644 --- a/packages/types/src/sdk/auth.ts +++ b/packages/types/src/sdk/auth.ts @@ -31,5 +31,5 @@ export interface ScannedSession { export interface PlatformLogoutOpts { ctx: BBContext userId: string - keepActiveSession: boolean + keepActiveSession?: boolean } diff --git a/packages/worker/__mocks__/node-fetch.ts b/packages/worker/__mocks__/node-fetch.ts deleted file mode 100644 index 4c7127ee48..0000000000 --- a/packages/worker/__mocks__/node-fetch.ts +++ /dev/null @@ -1 +0,0 @@ -jest.mock("node-fetch", () => jest.fn()) diff --git a/packages/worker/__mocks__/oauth.ts b/packages/worker/__mocks__/oauth.ts new file mode 100644 index 0000000000..8e8122a9e0 --- /dev/null +++ b/packages/worker/__mocks__/oauth.ts @@ -0,0 +1,57 @@ +import * as jwt from "jsonwebtoken" + +const mockOAuth2 = { + getOAuthAccessToken: (code: string, p: any, cb: any) => { + const err = null + const accessToken = "access_token" + const refreshToken = "refresh_token" + + const exp = new Date() + exp.setDate(exp.getDate() + 1) + + const iat = new Date() + iat.setDate(iat.getDate() - 1) + + const claims = { + iss: "test", + sub: "sub", + aud: "clientId", + exp: exp.getTime() / 1000, + iat: iat.getTime() / 1000, + email: "oauth@example.com", + } + + const idToken = jwt.sign(claims, "secret") + + const params = { + id_token: idToken, + } + return cb(err, accessToken, refreshToken, params) + }, + _request: ( + method: string, + url: string, + headers: any, + postBody: any, + accessToken: string, + cb: any + ) => { + const err = null + const body = { + sub: "sub", + user_id: "userId", + name: "OAuth", + family_name: "2", + given_name: "OAuth", + middle_name: "", + } + const res = {} + return cb(err, JSON.stringify(body), res) + }, +} + +const oauth = { + OAuth2: jest.fn(() => mockOAuth2), +} + +export = oauth diff --git a/packages/worker/package.json b/packages/worker/package.json index a9a093e753..c925b9d5d6 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -71,9 +71,11 @@ }, "devDependencies": { "@types/jest": "26.0.23", + "@types/jsonwebtoken": "8.5.1", "@types/koa": "2.13.4", "@types/koa__router": "8.0.11", "@types/node": "14.18.20", + "@types/node-fetch": "2.6.1", "@types/pouchdb": "6.4.0", "@types/uuid": "8.3.4", "@typescript-eslint/parser": "5.12.0", diff --git a/packages/worker/src/api/controllers/global/auth.ts b/packages/worker/src/api/controllers/global/auth.ts index c27fe17ee7..9065267658 100644 --- a/packages/worker/src/api/controllers/global/auth.ts +++ b/packages/worker/src/api/controllers/global/auth.ts @@ -1,4 +1,4 @@ -const core = require("@budibase/backend-core") +import core from "@budibase/backend-core" const { Configs, EmailTemplatePurpose } = require("../../../constants") const { sendEmail, isEmailConfigured } = require("../../../utilities/email") const { setCookie, getCookie, clearCookie, hash, platformLogout } = core.utils diff --git a/packages/worker/src/api/routes/global/auth.js b/packages/worker/src/api/routes/global/auth.js index 674279a6f4..2bf6bb68bf 100644 --- a/packages/worker/src/api/routes/global/auth.js +++ b/packages/worker/src/api/routes/global/auth.js @@ -29,11 +29,13 @@ function buildResetUpdateValidation() { } router + // PASSWORD .post( "/api/global/auth/:tenantId/login", buildAuthValidation(), authController.authenticate ) + .post("/api/global/auth/logout", authController.logout) .post( "/api/global/auth/:tenantId/reset", buildResetValidation(), @@ -44,36 +46,43 @@ router buildResetUpdateValidation(), authController.resetUpdate ) - .post("/api/global/auth/logout", authController.logout) + // INIT .post("/api/global/auth/init", authController.setInitInfo) .get("/api/global/auth/init", authController.getInitInfo) - .get("/api/global/auth/:tenantId/google", authController.googlePreAuth) + + // DATASOURCE - MULTI TENANT .get( "/api/global/auth/:tenantId/datasource/:provider", authController.datasourcePreAuth ) - // single tenancy endpoint - .get("/api/global/auth/google/callback", authController.googleAuth) - .get( - "/api/global/auth/datasource/:provider/callback", - authController.datasourceAuth - ) - // multi-tenancy endpoint - .get("/api/global/auth/:tenantId/google/callback", authController.googleAuth) .get( "/api/global/auth/:tenantId/datasource/:provider/callback", authController.datasourceAuth ) + + // DATASOURCE - SINGLE TENANT - DEPRECATED + .get( + "/api/global/auth/datasource/:provider/callback", + authController.datasourceAuth + ) + + // GOOGLE - MULTI TENANT + .get("/api/global/auth/:tenantId/google", authController.googlePreAuth) + .get("/api/global/auth/:tenantId/google/callback", authController.googleAuth) + + // GOOGLE - SINGLE TENANT - DEPRECATED + .get("/api/global/auth/google/callback", authController.googleAuth) + .get("/api/admin/auth/google/callback", authController.googleAuth) + + // OIDC - MULTI TENANT .get( "/api/global/auth/:tenantId/oidc/configs/:configId", authController.oidcPreAuth ) - // single tenancy endpoint - .get("/api/global/auth/oidc/callback", authController.oidcAuth) - // multi-tenancy endpoint .get("/api/global/auth/:tenantId/oidc/callback", authController.oidcAuth) - // deprecated - used by the default system before tenancy - .get("/api/admin/auth/google/callback", authController.googleAuth) + + // OIDC - SINGLE TENANT - DEPRECATED + .get("/api/global/auth/oidc/callback", authController.oidcAuth) .get("/api/admin/auth/oidc/callback", authController.oidcAuth) module.exports = router diff --git a/packages/worker/src/api/routes/global/tests/auth.spec.ts b/packages/worker/src/api/routes/global/tests/auth.spec.ts index 45c8a62cc7..0d47857ac1 100644 --- a/packages/worker/src/api/routes/global/tests/auth.spec.ts +++ b/packages/worker/src/api/routes/global/tests/auth.spec.ts @@ -3,6 +3,12 @@ import { TestConfiguration, mocks } from "../../../../tests" const sendMailMock = mocks.email.mock() import { events } from "@budibase/backend-core" +const expectSetAuthCookie = (res: any) => { + expect( + res.get("Set-Cookie").find((c: string) => c.startsWith("budibase:auth")) + ).toBeDefined() +} + describe("/api/global/auth", () => { const config = new TestConfiguration() @@ -18,92 +24,155 @@ describe("/api/global/auth", () => { jest.clearAllMocks() }) - it("should logout", async () => { - await config.api.auth.logout() - expect(events.auth.logout).toBeCalledTimes(1) - }) - - it("should be able to generate password reset email", async () => { - const { res, code } = await config.api.auth.requestPasswordReset( - sendMailMock - ) - const user = await config.getUser("test@test.com") - - expect(res.body).toEqual({ - message: "Please check your email for a reset link.", + describe("password", () => { + describe("POST /api/global/auth/:tenantId/login", () => { + it("should login", () => {}) }) - expect(sendMailMock).toHaveBeenCalled() - expect(code).toBeDefined() - expect(events.user.passwordResetRequested).toBeCalledTimes(1) - expect(events.user.passwordResetRequested).toBeCalledWith(user) + describe("POST /api/global/auth/logout", () => { + it("should logout", async () => { + await config.api.auth.logout() + expect(events.auth.logout).toBeCalledTimes(1) + + // TODO: Verify sessions deleted + }) + }) + + describe("POST /api/global/auth/:tenantId/reset", () => { + it("should generate password reset email", async () => { + const { res, code } = await config.api.auth.requestPasswordReset( + sendMailMock + ) + const user = await config.getUser("test@test.com") + + expect(res.body).toEqual({ + message: "Please check your email for a reset link.", + }) + expect(sendMailMock).toHaveBeenCalled() + + expect(code).toBeDefined() + expect(events.user.passwordResetRequested).toBeCalledTimes(1) + expect(events.user.passwordResetRequested).toBeCalledWith(user) + }) + }) + + describe("POST /api/global/auth/:tenantId/reset/update", () => { + it("should reset password", async () => { + const { code } = await config.api.auth.requestPasswordReset( + sendMailMock + ) + const user = await config.getUser("test@test.com") + delete user.password + + const res = await config.api.auth.updatePassword(code) + + expect(res.body).toEqual({ message: "password reset successfully." }) + expect(events.user.passwordReset).toBeCalledTimes(1) + expect(events.user.passwordReset).toBeCalledWith(user) + + // TODO: Login using new password + }) + }) }) - it("should allow resetting user password with code", async () => { - const { code } = await config.api.auth.requestPasswordReset(sendMailMock) - const user = await config.getUser("test@test.com") - delete user.password + describe("init", () => { + describe("POST /api/global/auth/init", () => {}) - const res = await config.api.auth.updatePassword(code) + describe("GET /api/global/auth/init", () => {}) + }) - expect(res.body).toEqual({ message: "password reset successfully." }) - expect(events.user.passwordReset).toBeCalledTimes(1) - expect(events.user.passwordReset).toBeCalledWith(user) + describe("datasource", () => { + // MULTI TENANT + + describe("GET /api/global/auth/:tenantId/datasource/:provider", () => {}) + + describe("GET /api/global/auth/:tenantId/datasource/:provider/callback", () => {}) + + // SINGLE TENANT + + describe("GET /api/global/auth/datasource/:provider/callback", () => {}) + }) + + describe("google", () => { + // MULTI TENANT + + describe("GET /api/global/auth/:tenantId/google", () => {}) + + describe("GET /api/global/auth/:tenantId/google/callback", () => {}) + + // SINGLE TENANT + + describe("GET /api/global/auth/google/callback", () => {}) + + describe("GET /api/admin/auth/google/callback", () => {}) }) describe("oidc", () => { - const auth = require("@budibase/backend-core/auth") - - const passportSpy = jest.spyOn(auth.passport, "authenticate") - let oidcConf - let chosenConfig: any - let configId: string - - // mock the oidc strategy implementation and return value - let strategyFactory = jest.fn() - let mockStrategyReturn = jest.fn() - let mockStrategyConfig = jest.fn() - auth.oidc.fetchStrategyConfig = mockStrategyConfig - - strategyFactory.mockReturnValue(mockStrategyReturn) - auth.oidc.strategyFactory = strategyFactory - beforeEach(async () => { - oidcConf = await config.saveOIDCConfig() - chosenConfig = oidcConf.config.configs[0] - configId = chosenConfig.uuid - mockStrategyConfig.mockReturnValue(chosenConfig) + jest.clearAllMocks() + mockGetWellKnownConfig() + + // see: __mocks__/oauth + // for associated mocking inside passport }) - afterEach(() => { - expect(strategyFactory).toBeCalledWith(chosenConfig, expect.any(Function)) - }) + const generateOidcConfig = async () => { + const oidcConf = await config.saveOIDCConfig() + const chosenConfig = oidcConf.config.configs[0] + return chosenConfig.uuid + } - describe("oidc configs", () => { - it("should load strategy and delegate to passport", async () => { - await config.api.configs.getOIDCConfig(configId) + const mockGetWellKnownConfig = () => { + mocks.fetch.mockReturnValue({ + ok: true, + json: () => ({ + issuer: "test", + authorization_endpoint: "http://localhost/auth", + token_endpoint: "http://localhost/token", + userinfo_endpoint: "http://localhost/userinfo", + }), + }) + } - expect(passportSpy).toBeCalledWith(mockStrategyReturn, { - scope: ["profile", "email", "offline_access"], - }) - expect(passportSpy.mock.calls.length).toBe(1) + // MULTI TENANT + describe("GET /api/global/auth/:tenantId/oidc/configs/:configId", () => { + it("redirects to auth provider", async () => { + const configId = await generateOidcConfig() + + const res = await config.api.configs.getOIDCConfig(configId) + + expect(res.status).toBe(302) + const location: string = res.get("location") + expect( + location.startsWith( + "http://localhost/auth?response_type=code&client_id=clientId&redirect_uri=http%3A%2F%2Flocalhost%3A10000%2Fapi%2Fglobal%2Fauth%2Fdefault%2Foidc%2Fcallback&scope=openid%20profile%20email%20offline_access" + ) + ).toBe(true) }) }) - describe("oidc callback", () => { - it("should load strategy and delegate to passport", async () => { - await config.api.configs.OIDCCallback(configId) + describe("GET /api/global/auth/:tenantId/oidc/callback", () => { + it("logs in", async () => { + const configId = await generateOidcConfig() + const preAuthRes = await config.api.configs.getOIDCConfig(configId) - expect(passportSpy).toBeCalledWith( - mockStrategyReturn, - { - successRedirect: "/", - failureRedirect: "/error", - }, - expect.anything() - ) - expect(passportSpy.mock.calls.length).toBe(1) + const res = await config.api.configs.OIDCCallback(configId, preAuthRes) + + expect(events.auth.login).toBeCalledWith("oidc") + expect(events.auth.login).toBeCalledTimes(1) + expect(res.status).toBe(302) + const location: string = res.get("location") + expect(location).toBe("/") + expectSetAuthCookie(res) }) }) + + // SINGLE TENANT + + describe("GET /api/global/auth/oidc/callback", () => {}) + + describe("GET /api/global/auth/oidc/callback", () => {}) + + describe("GET /api/admin/auth/oidc/callback", () => {}) }) }) diff --git a/packages/worker/src/api/routes/system/tests/tenants.spec.ts b/packages/worker/src/api/routes/system/tests/tenants.spec.ts index 8b3bcc99f5..af509b402e 100644 --- a/packages/worker/src/api/routes/system/tests/tenants.spec.ts +++ b/packages/worker/src/api/routes/system/tests/tenants.spec.ts @@ -1,7 +1,7 @@ import { TestConfiguration } from "../../../../tests" import { tenancy } from "@budibase/backend-core" -describe("/api/global/workspaces", () => { +describe("/api/global/tenants", () => { const config = new TestConfiguration() beforeAll(async () => { diff --git a/packages/worker/src/tests/TestConfiguration.ts b/packages/worker/src/tests/TestConfiguration.ts index 2927915284..11da7c2b03 100644 --- a/packages/worker/src/tests/TestConfiguration.ts +++ b/packages/worker/src/tests/TestConfiguration.ts @@ -171,8 +171,11 @@ class TestConfiguration { } cookieHeader(cookies: any) { + if (!Array.isArray(cookies)) { + cookies = [cookies] + } return { - Cookie: [cookies], + Cookie: cookies, } } @@ -288,11 +291,6 @@ class TestConfiguration { // CONFIGS - OIDC - getOIDConfigCookie(configId: string) { - const token = auth.jwt.sign(configId, env.JWT_SECRET) - return this.cookieHeader([[`${Cookies.OIDC_CONFIG}=${token}`]]) - } - async saveOIDCConfig() { await this.deleteConfig(Configs.OIDC) const config = structures.configs.oidc() diff --git a/packages/worker/src/tests/api/configs.ts b/packages/worker/src/tests/api/configs.ts index 6799229f58..10413dfdd6 100644 --- a/packages/worker/src/tests/api/configs.ts +++ b/packages/worker/src/tests/api/configs.ts @@ -23,10 +23,20 @@ export class ConfigAPI extends TestAPI { .expect(200) } - OIDCCallback = (configId: string) => { + OIDCCallback = (configId: string, preAuthRes: any) => { + const cookie = this.config.cookieHeader(preAuthRes.get("set-cookie")) + const setKoaSession = cookie.Cookie.find((c: string) => + c.includes("koa:sess") + ) + const koaSession = setKoaSession.split("=")[1] + "==" + const sessionContent = JSON.parse( + Buffer.from(koaSession, "base64").toString("utf-8") + ) + const handle = sessionContent["openidconnect:localhost"].state.handle return this.request .get(`/api/global/auth/${this.config.getTenantId()}/oidc/callback`) - .set(this.config.getOIDConfigCookie(configId)) + .query({ code: "test", state: handle }) + .set(cookie) } getOIDCConfig = (configId: string) => { diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index cfdba32e7a..23f54ae770 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -1284,6 +1284,13 @@ resolved "https://registry.yarnpkg.com/@types/json-buffer/-/json-buffer-3.0.0.tgz#85c1ff0f0948fc159810d4b5be35bf8c20875f64" integrity sha512-3YP80IxxFJB4b5tYC2SUPwkg0XQLiu0nWvhRgEatgjf+29IcWO9X1k8xRv5DGssJ/lCrjYTjQPcobJr2yWIVuQ== +"@types/jsonwebtoken@8.5.1": + version "8.5.1" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#56958cb2d80f6d74352bd2e501a018e2506a8a84" + integrity sha512-rNAPdomlIUX0i0cg2+I+Q1wOUr531zHBQ+cV/28PJ39bSPKjahatZZ2LMuhiguETkCgLVzfruw/ZvNMNkKoSzw== + dependencies: + "@types/node" "*" + "@types/keygrip@*": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" @@ -1334,6 +1341,14 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== +"@types/node-fetch@2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.1.tgz#8f127c50481db65886800ef496f20bbf15518975" + integrity sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA== + dependencies: + "@types/node" "*" + form-data "^3.0.0" + "@types/node@*": version "17.0.41" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.41.tgz#1607b2fd3da014ae5d4d1b31bc792a39348dfb9b" @@ -3412,6 +3427,15 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" diff --git a/qa-core/src/config/internal-api/TestConfiguration/applications.ts b/qa-core/src/config/internal-api/TestConfiguration/applications.ts index cb0558222e..13d0969854 100644 --- a/qa-core/src/config/internal-api/TestConfiguration/applications.ts +++ b/qa-core/src/config/internal-api/TestConfiguration/applications.ts @@ -114,8 +114,6 @@ export default class AppApi { return [response, json] } - - async delete(appId: string): Promise<[Response, any]> { const response = await this.api.del(`/applications/${appId}`) const json = await response.json() @@ -123,7 +121,11 @@ export default class AppApi { return [response, json] } - async update(appId: string, oldName: string, body: any): Promise<[Response, Application]> { + async update( + appId: string, + oldName: string, + body: any + ): Promise<[Response, Application]> { const response = await this.api.put(`/applications/${appId}`, { body }) const json = await response.json() expect(response).toHaveStatusCode(200) @@ -142,7 +144,6 @@ export default class AppApi { const json = await response.json() expect(response).toHaveStatusCode(200) if (screenExists) { - expect(json.routes["/test"]).toBeTruthy() } else { expect(json.routes["/test"]).toBeUndefined() diff --git a/qa-core/src/config/internal-api/TestConfiguration/tables.ts b/qa-core/src/config/internal-api/TestConfiguration/tables.ts index ed0ab78cad..5b7e1648a0 100644 --- a/qa-core/src/config/internal-api/TestConfiguration/tables.ts +++ b/qa-core/src/config/internal-api/TestConfiguration/tables.ts @@ -46,9 +46,7 @@ export default class TablesApi { const response = await this.api.del(`/tables/${id}/${revId}`) const json = await response.json() expect(response).toHaveStatusCode(200) - expect(json.message).toEqual( - `Table ${id} deleted.` - ) + expect(json.message).toEqual(`Table ${id} deleted.`) return [response, json] } } diff --git a/qa-core/src/config/internal-api/fixtures/applications.ts b/qa-core/src/config/internal-api/fixtures/applications.ts index abdd674577..200aa9abff 100644 --- a/qa-core/src/config/internal-api/fixtures/applications.ts +++ b/qa-core/src/config/internal-api/fixtures/applications.ts @@ -1,7 +1,6 @@ import generator from "../../generator" import { Application } from "@budibase/server/api/controllers/public/mapping/types" - const generate = ( overrides: Partial = {} ): Partial => ({ diff --git a/qa-core/src/tests/internal-api/applications/applications.spec.ts b/qa-core/src/tests/internal-api/applications/applications.spec.ts index 4b9b66ec65..4b3208ee10 100644 --- a/qa-core/src/tests/internal-api/applications/applications.spec.ts +++ b/qa-core/src/tests/internal-api/applications/applications.spec.ts @@ -48,7 +48,8 @@ describe("Internal API - Application creation, update, publish and delete", () = }) config.applications.api.appId = app.appId - const [appPackageResponse, appPackageJson] = await config.applications.getAppPackage(app.appId) + const [appPackageResponse, appPackageJson] = + await config.applications.getAppPackage(app.appId) expect(appPackageJson.application.name).toEqual(app.name) expect(appPackageJson.application.version).toEqual(app.version) expect(appPackageJson.application.url).toEqual(app.url) @@ -72,7 +73,6 @@ describe("Internal API - Application creation, update, publish and delete", () = config.applications.api.appId = db.getProdAppID(app.appId) await config.applications.canRender() - // unpublish app await config.applications.unpublish(app.appId) }) @@ -109,22 +109,16 @@ describe("Internal API - Application creation, update, publish and delete", () = config.applications.api.appId = app.appId - await config.applications.update( - app.appId, - app.name, - { - name: generator.word(), - } - ) + await config.applications.update(app.appId, app.name, { + name: generator.word(), + }) }) it("POST - Revert Changes without changes", async () => { const app = await config.applications.create(generateApp()) config.applications.api.appId = app.appId - await config.applications.revertUnpublished( - app.appId - ) + await config.applications.revertUnpublished(app.appId) }) it("POST - Revert Changes", async () => { @@ -134,20 +128,14 @@ describe("Internal API - Application creation, update, publish and delete", () = // publish app await config.applications.publish(app.url) - // Change/add component to the app - await config.screen.create( - generateScreen("BASIC") - ) + await config.screen.create(generateScreen("BASIC")) // // Revert the app to published state - await config.applications.revertPublished( - app.appId - ) + await config.applications.revertPublished(app.appId) // Check screen is removed await config.applications.getRoutes() - }) it("DELETE - Delete an application", async () => { @@ -155,5 +143,4 @@ describe("Internal API - Application creation, update, publish and delete", () = await config.applications.delete(app.appId) }) - }) diff --git a/qa-core/src/tests/internal-api/screens/screens.spec.ts b/qa-core/src/tests/internal-api/screens/screens.spec.ts index 2dc7962914..218d71cb0d 100644 --- a/qa-core/src/tests/internal-api/screens/screens.spec.ts +++ b/qa-core/src/tests/internal-api/screens/screens.spec.ts @@ -38,9 +38,7 @@ describe("Internal API - /screens endpoints", () => { // Create Screen appConfig.applications.api.appId = app.appId - await config.screen.create( - generateScreen("BASIC") - ) + await config.screen.create(generateScreen("BASIC")) // Check screen exists await appConfig.applications.getRoutes(true) @@ -58,6 +56,5 @@ describe("Internal API - /screens endpoints", () => { // Delete Screen await config.screen.delete(screen._id!, screen._rev!) - }) }) diff --git a/qa-core/src/tests/internal-api/tables/tables.spec.ts b/qa-core/src/tests/internal-api/tables/tables.spec.ts index 69ad0fed7b..4f9e4299cf 100644 --- a/qa-core/src/tests/internal-api/tables/tables.spec.ts +++ b/qa-core/src/tests/internal-api/tables/tables.spec.ts @@ -3,93 +3,87 @@ import { Application } from "@budibase/server/api/controllers/public/mapping/typ import InternalAPIClient from "../../../config/internal-api/TestConfiguration/InternalAPIClient" import generator from "../../../config/generator" import { - generateTable, - generateNewColumnForTable, + generateTable, + generateNewColumnForTable, } from "../../../config/internal-api/fixtures/table" import { generateNewRowForTable } from "../../../config/internal-api/fixtures/rows" describe("Internal API - Application creation, update, publish and delete", () => { - const api = new InternalAPIClient() - const config = new TestConfiguration(api) + const api = new InternalAPIClient() + const config = new TestConfiguration(api) - beforeAll(async () => { - await config.beforeAll() + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + async function createAppFromTemplate() { + return config.applications.create({ + name: generator.word(), + url: `/${generator.word()}`, + useTemplate: "true", + templateName: "Near Miss Register", + templateKey: "app/near-miss-register", + templateFile: undefined, }) + } - afterAll(async () => { - await config.afterAll() - }) + it("Operations on Tables", async () => { + // create the app + const appName = generator.word() + const app = await createAppFromTemplate() + config.applications.api.appId = app.appId - async function createAppFromTemplate() { - return config.applications.create({ - name: generator.word(), - url: `/${generator.word()}`, - useTemplate: "true", - templateName: "Near Miss Register", - templateKey: "app/near-miss-register", - templateFile: undefined, - }) + // Get current tables: expect 2 in this template + await config.tables.getAll(2) + + // Add new table + const [createdTableResponse, createdTableData] = await config.tables.save( + generateTable() + ) + + //Table was added + await config.tables.getAll(3) + + //Get information about the table + await config.tables.getTableById(createdTableData._id) + + //Add Column to table + const newColumn = generateNewColumnForTable(createdTableData) + const [addColumnResponse, addColumnData] = await config.tables.save( + newColumn, + true + ) + + //Add Row to table + const newRow = generateNewRowForTable(addColumnData._id) + await config.rows.add(addColumnData._id, newRow) + + //Get Row from table + const [getRowResponse, getRowData] = await config.rows.getAll( + addColumnData._id + ) + + //Delete Row from table + const rowToDelete = { + rows: [getRowData[0]], } + const [deleteRowResponse, deleteRowData] = await config.rows.delete( + addColumnData._id, + rowToDelete + ) + expect(deleteRowData[0]._id).toEqual(getRowData[0]._id) - it("Operations on Tables", async () => { - // create the app - const appName = generator.word() - const app = await createAppFromTemplate() - config.applications.api.appId = app.appId + //Delete the table + const [deleteTableResponse, deleteTable] = await config.tables.delete( + addColumnData._id, + addColumnData._rev + ) - // Get current tables: expect 2 in this template - await config.tables.getAll(2) - - // Add new table - const [createdTableResponse, createdTableData] = await config.tables.save( - generateTable() - ) - - //Table was added - await config.tables.getAll(3) - - //Get information about the table - await config.tables.getTableById( - createdTableData._id - ) - - //Add Column to table - const newColumn = generateNewColumnForTable(createdTableData) - const [addColumnResponse, addColumnData] = await config.tables.save( - newColumn, - true - ) - - //Add Row to table - const newRow = generateNewRowForTable(addColumnData._id) - await config.rows.add( - addColumnData._id, - newRow - ) - - //Get Row from table - const [getRowResponse, getRowData] = await config.rows.getAll( - addColumnData._id - ) - - //Delete Row from table - const rowToDelete = { - rows: [getRowData[0]], - } - const [deleteRowResponse, deleteRowData] = await config.rows.delete( - addColumnData._id, - rowToDelete - ) - expect(deleteRowData[0]._id).toEqual(getRowData[0]._id) - - //Delete the table - const [deleteTableResponse, deleteTable] = await config.tables.delete( - addColumnData._id, - addColumnData._rev - ) - - - //Table was deleted - await config.tables.getAll(2) - }) + //Table was deleted + await config.tables.getAll(2) + }) })