diff --git a/.travis.yml b/.travis.yml index 16cdbb35bf3a5..a02d009f85252 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,8 @@ matrix: env: TOXENV=py36-sqlite - python: 3.6 env: TOXENV=pylint + - python: 3.6 + env: TOXENV=cypress exclude: - python: 2.7 - python: 3.6 diff --git a/superset/assets/cypress.json b/superset/assets/cypress.json new file mode 100644 index 0000000000000..c84e05d6d3211 --- /dev/null +++ b/superset/assets/cypress.json @@ -0,0 +1,3 @@ +{ + "baseUrl": "http://localhost:8081" +} diff --git a/superset/assets/cypress/integration/dashboard/dashboard_tests.js b/superset/assets/cypress/integration/dashboard/dashboard_tests.js new file mode 100644 index 0000000000000..10e4f1132f395 --- /dev/null +++ b/superset/assets/cypress/integration/dashboard/dashboard_tests.js @@ -0,0 +1,26 @@ +describe('Load dashboard', function () { + it('Load birth names dashboard', function () { + cy.server(); + cy.login(); + + cy.visit('/superset/dashboard/births'); + + cy.route('POST', '/superset/explore_json/**').as('getJson'); + cy.wait(10000, ['@getJson']); + + let sliceData; + + cy.get('@getJson.all').then((xhrs) => { + sliceData = xhrs; + xhrs.forEach((data) => { + expect(data.status).to.eq(200); + expect(data.response.body).to.have.property('error', null); + cy.get(`#slice-container-${data.response.body.form_data.slice_id}`); + }); + cy.get('#app').then((data) => { + const bootstrapData = JSON.parse(data[0].dataset.bootstrap); + expect(bootstrapData.dashboard_data.slices.length).to.eq(sliceData.length); + }); + }); + }); +}); diff --git a/superset/assets/cypress/integration/explore/control_tests.js b/superset/assets/cypress/integration/explore/control_tests.js new file mode 100644 index 0000000000000..d4c5e4cfb657e --- /dev/null +++ b/superset/assets/cypress/integration/explore/control_tests.js @@ -0,0 +1,59 @@ +// *********************************************** +// Tests for setting controls in the UI +// *********************************************** + +describe('Groupby', function () { + it('Set groupby', function () { + cy.server(); + cy.login(); + + cy.route('POST', '/superset/explore_json/**').as('getJson'); + cy.visitChartByName('Num Births Trend'); + cy.verifySliceSuccess('@getJson'); + + cy.get('[data-test=groupby]').within(() => { + cy.get('.Select-control').click(); + cy.get('input.select-input').type('state', { force: true }); + cy.get('.VirtualizedSelectFocusedOption').click(); + }); + cy.get('button.query').click(); + cy.verifySliceSuccess('@getJson'); + }); +}); + +describe('SimpleAdhocMetric', function () { + it('Clear metric and set simple adhoc metric', function () { + cy.server(); + cy.login(); + + const metricName = 'Girl Births'; + + cy.route('POST', '/superset/explore_json/**').as('getJson'); + cy.visitChartByName('Num Births Trend'); + cy.verifySliceSuccess('@getJson'); + + cy.get('[data-test=metrics]').within(() => { + cy.get('.select-clear').click(); + cy.get('.Select-control').click({ force: true }); + cy.get('input').type('sum_girls', { force: true }); + cy.get('.VirtualizedSelectFocusedOption').trigger('mousedown').click(); + }); + + cy.get('#metrics-edit-popover').within(() => { + cy.get('.popover-title').within(() => { + cy.get('span').click(); + cy.get('input').type(metricName); + }); + cy.get('button').contains('Save').click(); + }); + + cy.get('button.query').click(); + cy.wait(['@getJson']).then((data) => { + expect(data.status).to.eq(200); + expect(data.response.body).to.have.property('error', null); + expect(data.response.body.data[0].key).to.equal(metricName); + cy.get('.slice_container'); + }); + }); +}); + diff --git a/superset/assets/cypress/integration/explore/visualization_tests.js b/superset/assets/cypress/integration/explore/visualization_tests.js new file mode 100644 index 0000000000000..50c331dbb468c --- /dev/null +++ b/superset/assets/cypress/integration/explore/visualization_tests.js @@ -0,0 +1,54 @@ +// *********************************************** +// Tests for visualization types +// *********************************************** + +const FORM_DATA_DEFAULTS = { + datasource: '3__table', + viz_type: 'line', + granularity_sqla: 'ds', + time_grain_sqla: null, + time_range: '100+years+ago+:+now', + adhoc_filters: [], + groupby: [], + limit: null, + timeseries_limit_metric: null, + order_desc: false, + contribution: false, +}; + +describe('Line', function () { + it('Test line chart with adhoc metric', function () { + cy.server(); + cy.login(); + + const metrics = [{ + expressionType: 'SIMPLE', + column: { + id: 336, + column_name: 'num', + verbose_name: null, + description: null, + expression: '', + filterable: false, + groupby: false, + is_dttm: false, + type: 'BIGINT', + database_expression: null, + python_date_format: null, + optionName: '_col_num', + }, + aggregate: 'SUM', + sqlExpression: null, + hasCustomLabel: false, + fromFormData: false, + label: 'SUM(num)', + optionName: 'metric_1de0s4viy5d_ly7y8k6ghvk', + }]; + + const formData = { ...FORM_DATA_DEFAULTS, metrics }; + + cy.route('POST', '/superset/explore_json/**').as('getJson'); + cy.visitChartByParams(JSON.stringify(formData)); + cy.verifySliceSuccess('@getJson'); + }); +}); diff --git a/superset/assets/cypress/plugins/index.js b/superset/assets/cypress/plugins/index.js new file mode 100644 index 0000000000000..df3a5aeeaf1ec --- /dev/null +++ b/superset/assets/cypress/plugins/index.js @@ -0,0 +1,17 @@ +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +module.exports = (/* on, config */) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +}; diff --git a/superset/assets/cypress/support/commands.js b/superset/assets/cypress/support/commands.js new file mode 100644 index 0000000000000..41c64a7ab1eaf --- /dev/null +++ b/superset/assets/cypress/support/commands.js @@ -0,0 +1,59 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This is will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) + +const BASE_EXPLORE_URL = '/superset/explore/?form_data='; + +Cypress.Commands.add('login', () => { + cy.request({ + method: 'POST', + url: 'http://localhost:8081/login/', + body: { username: 'admin', password: 'general' }, + }).then((response) => { + expect(response.status).to.eq(200); + }); +}); + +Cypress.Commands.add('visitChartByName', (name) => { + cy.request(`http://localhost:8081/chart/api/read?_flt_3_slice_name=${name}`).then((response) => { + cy.visit(`${BASE_EXPLORE_URL}{"slice_id": ${response.body.pks[0]}}`); + }); +}); + +Cypress.Commands.add('visitChartById', (chartId) => { + cy.visit(`${BASE_EXPLORE_URL}{"slice_id": ${chartId}}`); +}); + +Cypress.Commands.add('visitChartByParams', (params) => { + cy.visit(`${BASE_EXPLORE_URL}${params}`); +}); + +Cypress.Commands.add('verifySliceSuccess', (waitAlias) => { + cy.wait([waitAlias]).then((data) => { + expect(data.status).to.eq(200); + expect(data.response.body).to.have.property('error', null); + cy.get('.slice_container'); + }); +}); diff --git a/superset/assets/cypress/support/index.js b/superset/assets/cypress/support/index.js new file mode 100644 index 0000000000000..37a498fb5bf39 --- /dev/null +++ b/superset/assets/cypress/support/index.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/superset/assets/cypress_build.sh b/superset/assets/cypress_build.sh new file mode 100755 index 0000000000000..dd80e873880a2 --- /dev/null +++ b/superset/assets/cypress_build.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e + +superset/bin/superset db upgrade +superset/bin/superset load_test_users +superset/bin/superset load_examples +superset/bin/superset init +superset/bin/superset runserver & + +cd "$(dirname "$0")" + +npm install -g yarn +yarn +npm run build +npm run cypress run +kill %1 diff --git a/superset/assets/package.json b/superset/assets/package.json index 13122feefcc83..f36ffdeb5db44 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -17,7 +17,8 @@ "build": "webpack --mode=production --colors --progress", "lint": "eslint --ignore-path=.eslintignore --ext .js,.jsx .", "lint-fix": "eslint --fix --ignore-path=.eslintignore --ext .js,.jsx .", - "sync-backend": "babel-node --presets env src/syncBackend.js" + "sync-backend": "babel-node --presets env src/syncBackend.js", + "cypress": "cypress" }, "repository": { "type": "git", @@ -141,6 +142,7 @@ "chai": "^4.0.2", "clean-webpack-plugin": "^0.1.19", "css-loader": "^0.28.0", + "cypress": "^3.0.3", "enzyme": "^2.0.0", "eslint": "^4.19.0", "eslint-config-airbnb": "^15.0.1", diff --git a/superset/assets/src/explore/components/Control.jsx b/superset/assets/src/explore/components/Control.jsx index 52682dee00222..bc58ec2b0edd1 100644 --- a/superset/assets/src/explore/components/Control.jsx +++ b/superset/assets/src/explore/components/Control.jsx @@ -84,6 +84,7 @@ export default class Control extends React.PureComponent { const divStyle = this.props.hidden ? { display: 'none' } : null; return (
=0.5 0", mkdirp@^0.5, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.1" resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -8537,6 +8785,15 @@ optionator@^0.8.1, optionator@^0.8.2: type-check "~0.3.2" wordwrap "~1.0.0" +ora@^0.2.3: + version "0.2.3" + resolved "http://registry.npmjs.org/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4" + dependencies: + chalk "^1.1.1" + cli-cursor "^1.0.2" + cli-spinners "^0.1.2" + object-assign "^4.0.1" + original@>=0.0.5: version "1.0.2" resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" @@ -8547,7 +8804,7 @@ os-browserify@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" -os-homedir@^1.0.0: +os-homedir@^1.0.0, os-homedir@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" @@ -8559,7 +8816,7 @@ os-locale@^2.0.0: lcid "^1.0.0" mem "^1.1.0" -os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -8851,6 +9108,10 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + performance-now@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" @@ -8863,7 +9124,7 @@ phin@^2.9.1: version "2.9.1" resolved "https://registry.yarnpkg.com/phin/-/phin-2.9.1.tgz#0de9059b1a9bd56fcb1bd8a374344a06f25f1901" -pify@^2.0.0, pify@^2.3.0: +pify@^2.0.0, pify@^2.2.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -9468,6 +9729,10 @@ process@~0.5.1: version "0.5.2" resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf" +progress@1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" + progress@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" @@ -9648,6 +9913,10 @@ raf@^3.3.0: dependencies: performance-now "^2.1.0" +ramda@0.24.1: + version "0.24.1" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.24.1.tgz#c3b7755197f35b8dc3502228262c4c91ddb6b857" + randomatic@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.0.tgz#36f2ca708e9e567f5ed2ec01949026d50aa10116" @@ -10218,7 +10487,7 @@ readable-stream@~2.1.5: string_decoder "~0.10.x" util-deprecate "~1.0.1" -readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0: +readdir-scoped-modules@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747" dependencies: @@ -10461,30 +10730,36 @@ replace-ext@1.0.0, replace-ext@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" -request@2, request@^2.65.0, request@^2.74.0, request@^2.79.0, request@^2.85.0: - version "2.88.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" +request-progress@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-0.3.1.tgz#0721c105d8a96ac6b2ce8b2c89ae2d5ecfcf6b3a" + dependencies: + throttleit "~0.0.2" + +request@2, request@2.87.0, request@^2.74.0, request@^2.79.0: + version "2.87.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e" dependencies: aws-sign2 "~0.7.0" - aws4 "^1.8.0" + aws4 "^1.6.0" caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" + combined-stream "~1.0.5" + extend "~3.0.1" forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.0" + form-data "~2.3.1" + har-validator "~5.0.3" http-signature "~1.2.0" is-typedarray "~1.0.0" isstream "~0.1.2" json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" + mime-types "~2.1.17" + oauth-sign "~0.8.2" performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.4.3" + qs "~6.5.1" + safe-buffer "^5.1.1" + tough-cookie "~2.3.3" tunnel-agent "^0.6.0" - uuid "^3.3.2" + uuid "^3.1.0" request@2.81.0: version "2.81.0" @@ -10513,6 +10788,31 @@ request@2.81.0: tunnel-agent "^0.6.0" uuid "^3.0.0" +request@^2.65.0, request@^2.85.0: + version "2.88.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + request@~2.22.0: version "2.22.0" resolved "http://registry.npmjs.org/request/-/request-2.22.0.tgz#b883a769cc4a909571eb5004b344c43cf7e51592" @@ -10717,9 +11017,9 @@ rx-lite@*, rx-lite@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" -rxjs@^5.5.2: - version "5.5.12" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.12.tgz#6fa61b8a77c3d793dbaf270bee2f43f652d741cc" +rxjs@^5.0.0-beta.11, rxjs@^5.5.2: + version "5.5.11" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.11.tgz#f733027ca43e3bec6b994473be4ab98ad43ced87" dependencies: symbol-observable "1.0.1" @@ -11342,6 +11642,10 @@ stream-to-buffer@^0.1.0: dependencies: stream-to "~0.2.0" +stream-to-observable@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.1.0.tgz#45bf1d9f2d7dc09bed81f1c307c430e68b84cffe" + stream-to@~0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/stream-to/-/stream-to-0.2.2.tgz#84306098d85fdb990b9fa300b1b3ccf55e8ef01d" @@ -11475,6 +11779,12 @@ supports-color@3.1.2, supports-color@3.1.x: dependencies: has-flag "^1.0.0" +supports-color@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.1.0.tgz#058a021d1b619f7ddf3980d712ea3590ce7de3d5" + dependencies: + has-flag "^2.0.0" + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -11600,6 +11910,10 @@ textextensions@2: version "2.2.0" resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.2.0.tgz#38ac676151285b658654581987a0ce1a4490d286" +throttleit@~0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf" + through2@^2.0.0, through2@^2.0.3, through2@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" @@ -11648,6 +11962,12 @@ tinyqueue@^1.1.0: version "1.2.3" resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-1.2.3.tgz#b6a61de23060584da29f82362e45df1ec7353f3d" +tmp@0.0.31: + version "0.0.31" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" + dependencies: + os-tmpdir "~1.0.1" + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -11941,6 +12261,10 @@ unist-util-visit@^1.1.0, unist-util-visit@^1.3.0: dependencies: unist-util-visit-parents "^2.0.0" +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -12038,7 +12362,7 @@ url-to-options@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" -url@^0.11.0: +url@0.11.0, url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" dependencies: @@ -12098,7 +12422,7 @@ v8flags@^2.1.1: dependencies: user-home "^1.1.1" -validate-npm-package-license@*, validate-npm-package-license@^3.0.1: +validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" dependencies: @@ -12628,6 +12952,19 @@ yargs@~1.2.6: dependencies: minimist "^0.1.0" +yauzl@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" + dependencies: + fd-slicer "~1.0.1" + +yauzl@2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.8.0.tgz#79450aff22b2a9c5a41ef54e02db907ccfbf9ee2" + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.0.1" + yeoman-environment@^2.0.5, yeoman-environment@^2.1.1: version "2.3.3" resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.3.3.tgz#1bd9720714cc49036e901503a789d809df8f51bf" diff --git a/superset/cli.py b/superset/cli.py index 074e12f963102..68e3de7c159a6 100755 --- a/superset/cli.py +++ b/superset/cli.py @@ -20,6 +20,7 @@ from superset import ( app, data, db, dict_import_export_util, security_manager, utils, ) +from tests.utils import get_main_database config = app.config celery_app = utils.get_celery_app(config) @@ -365,3 +366,69 @@ def flower(port, address): print(Fore.YELLOW + cmd) print(Fore.BLUE + '-=' * 40) Popen(cmd, shell=True).wait() + + +@app.cli.command() +def load_test_users(): + """ + Loads admin, alpha, and gamma user for testing purposes + + Syncs permissions for those users/roles + """ + load_test_users_run() + + +def load_test_users_run(): + """ + Loads admin, alpha, and gamma user for testing purposes + + Syncs permissions for those users/roles + """ + if config.get('TESTING'): + security_manager.sync_role_definitions() + gamma_sqllab_role = security_manager.add_role('gamma_sqllab') + for perm in security_manager.find_role('Gamma').permissions: + security_manager.add_permission_role(gamma_sqllab_role, perm) + utils.get_or_create_main_db() + db_perm = get_main_database(security_manager.get_session).perm + security_manager.merge_perm('database_access', db_perm) + db_pvm = security_manager.find_permission_view_menu( + view_menu_name=db_perm, permission_name='database_access') + gamma_sqllab_role.permissions.append(db_pvm) + for perm in security_manager.find_role('sql_lab').permissions: + security_manager.add_permission_role(gamma_sqllab_role, perm) + + admin = security_manager.find_user('admin') + if not admin: + security_manager.add_user( + 'admin', 'admin', ' user', 'admin@fab.org', + security_manager.find_role('Admin'), + password='general') + + gamma = security_manager.find_user('gamma') + if not gamma: + security_manager.add_user( + 'gamma', 'gamma', 'user', 'gamma@fab.org', + security_manager.find_role('Gamma'), + password='general') + + gamma2 = security_manager.find_user('gamma2') + if not gamma2: + security_manager.add_user( + 'gamma2', 'gamma2', 'user', 'gamma2@fab.org', + security_manager.find_role('Gamma'), + password='general') + + gamma_sqllab_user = security_manager.find_user('gamma_sqllab') + if not gamma_sqllab_user: + security_manager.add_user( + 'gamma_sqllab', 'gamma_sqllab', 'user', 'gamma_sqllab@fab.org', + gamma_sqllab_role, password='general') + + alpha = security_manager.find_user('alpha') + if not alpha: + security_manager.add_user( + 'alpha', 'alpha', 'user', 'alpha@fab.org', + security_manager.find_role('Alpha'), + password='general') + security_manager.get_session.commit() diff --git a/superset/data/__init__.py b/superset/data/__init__.py index 0b232f751c017..56af4a6423b98 100644 --- a/superset/data/__init__.py +++ b/superset/data/__init__.py @@ -955,6 +955,14 @@ def load_birth_names(): 'aggregate': 'SUM', 'label': 'SUM(num_california)', })), + Slice( + slice_name="Num Births Trend", + viz_type='line', + datasource_type='table', + datasource_id=tbl.id, + params=get_slice_json( + defaults, + viz_type="line")), ] for slc in slices: merge_slice(slc) diff --git a/tests/base_tests.py b/tests/base_tests.py index 782cedd0a19e3..f89aaf76b95c4 100644 --- a/tests/base_tests.py +++ b/tests/base_tests.py @@ -14,10 +14,11 @@ from mock import Mock import pandas as pd -from superset import app, cli, db, security_manager, utils +from superset import app, cli, db, security_manager from superset.connectors.druid.models import DruidCluster, DruidDatasource from superset.connectors.sqla.models import SqlaTable from superset.models import core as models +from .utils import get_main_database BASE_DIR = app.config.get('BASE_DIR') @@ -43,52 +44,7 @@ def __init__(self, *args, **kwargs): self.client = app.test_client() self.maxDiff = None - gamma_sqllab_role = security_manager.add_role('gamma_sqllab') - for perm in security_manager.find_role('Gamma').permissions: - security_manager.add_permission_role(gamma_sqllab_role, perm) - utils.get_or_create_main_db() - db_perm = self.get_main_database(security_manager.get_session).perm - security_manager.merge_perm('database_access', db_perm) - db_pvm = security_manager.find_permission_view_menu( - view_menu_name=db_perm, permission_name='database_access') - gamma_sqllab_role.permissions.append(db_pvm) - for perm in security_manager.find_role('sql_lab').permissions: - security_manager.add_permission_role(gamma_sqllab_role, perm) - - admin = security_manager.find_user('admin') - if not admin: - security_manager.add_user( - 'admin', 'admin', ' user', 'admin@fab.org', - security_manager.find_role('Admin'), - password='general') - - gamma = security_manager.find_user('gamma') - if not gamma: - security_manager.add_user( - 'gamma', 'gamma', 'user', 'gamma@fab.org', - security_manager.find_role('Gamma'), - password='general') - - gamma2 = security_manager.find_user('gamma2') - if not gamma2: - security_manager.add_user( - 'gamma2', 'gamma2', 'user', 'gamma2@fab.org', - security_manager.find_role('Gamma'), - password='general') - - gamma_sqllab_user = security_manager.find_user('gamma_sqllab') - if not gamma_sqllab_user: - security_manager.add_user( - 'gamma_sqllab', 'gamma_sqllab', 'user', 'gamma_sqllab@fab.org', - gamma_sqllab_role, password='general') - - alpha = security_manager.find_user('alpha') - if not alpha: - security_manager.add_user( - 'alpha', 'alpha', 'user', 'alpha@fab.org', - security_manager.find_role('Alpha'), - password='general') - security_manager.get_session.commit() + cli.load_test_users_run() # create druid cluster and druid datasources session = db.session cluster = ( @@ -185,13 +141,6 @@ def get_json_resp( resp = self.get_resp(url, data, follow_redirects, raise_on_error) return json.loads(resp) - def get_main_database(self, session): - return ( - db.session.query(models.Database) - .filter_by(database_name='main') - .first() - ) - def get_access_requests(self, username, ds_type, ds_id): DAR = models.DatasourceAccessRequest return ( @@ -227,7 +176,7 @@ def run_sql(self, sql, client_id, user_name=None, raise_on_error=False): if user_name: self.logout() self.login(username=(user_name if user_name else 'admin')) - dbid = self.get_main_database(db.session).id + dbid = get_main_database(db.session).id resp = self.get_json_resp( '/superset/sql_json/', raise_on_error=False, diff --git a/tests/celery_tests.py b/tests/celery_tests.py index 243aacef4586d..7542abeb00376 100644 --- a/tests/celery_tests.py +++ b/tests/celery_tests.py @@ -19,6 +19,7 @@ from superset.models.sql_lab import Query from superset.sql_parse import SupersetQuery from .base_tests import SupersetTestCase +from .utils import get_main_database BASE_DIR = app.config.get('BASE_DIR') @@ -140,14 +141,14 @@ def run_sql(self, db_id, sql, client_id, cta='false', tmp_table='tmp', return json.loads(resp.data.decode('utf-8')) def test_run_sync_query_dont_exist(self): - main_db = self.get_main_database(db.session) + main_db = get_main_database(db.session) db_id = main_db.id sql_dont_exist = 'SELECT name FROM table_dont_exist' result1 = self.run_sql(db_id, sql_dont_exist, '1', cta='true') self.assertTrue('error' in result1) def test_run_sync_query_cta(self): - main_db = self.get_main_database(db.session) + main_db = get_main_database(db.session) db_id = main_db.id eng = main_db.get_sqla_engine() perm_name = 'can_sql_json' @@ -166,7 +167,7 @@ def test_run_sync_query_cta(self): self.assertEqual([{'name': perm_name}], data2) def test_run_sync_query_cta_no_data(self): - main_db = self.get_main_database(db.session) + main_db = get_main_database(db.session) db_id = main_db.id sql_empty_result = 'SELECT * FROM ab_user WHERE id=666' result3 = self.run_sql( @@ -179,7 +180,7 @@ def test_run_sync_query_cta_no_data(self): self.assertEqual(QueryStatus.SUCCESS, query3.status) def test_run_async_query(self): - main_db = self.get_main_database(db.session) + main_db = get_main_database(db.session) eng = main_db.get_sqla_engine() sql_where = "SELECT name FROM ab_role WHERE name='Admin'" result = self.run_sql( @@ -207,7 +208,7 @@ def test_run_async_query(self): self.assertEqual(True, query.select_as_cta_used) def test_run_async_query_with_lower_limit(self): - main_db = self.get_main_database(db.session) + main_db = get_main_database(db.session) eng = main_db.get_sqla_engine() sql_where = "SELECT name FROM ab_role WHERE name='Alpha' LIMIT 1" result = self.run_sql( diff --git a/tests/core_tests.py b/tests/core_tests.py index f03c51f2b392c..d4aeeceff28d7 100644 --- a/tests/core_tests.py +++ b/tests/core_tests.py @@ -29,6 +29,7 @@ from superset.models.sql_lab import Query from superset.views.core import DatabaseView from .base_tests import SupersetTestCase +from .utils import get_main_database class CoreTests(SupersetTestCase): @@ -312,7 +313,7 @@ def test_misc(self): def test_testconn(self, username='admin'): self.login(username=username) - database = self.get_main_database(db.session) + database = get_main_database(db.session) # validate that the endpoint works with the password-masked sqlalchemy uri data = json.dumps({ @@ -341,7 +342,7 @@ def test_testconn(self, username='admin'): assert response.headers['Content-Type'] == 'application/json' def test_custom_password_store(self): - database = self.get_main_database(db.session) + database = get_main_database(db.session) conn_pre = sqla.engine.url.make_url(database.sqlalchemy_uri_decrypted) def custom_password_store(uri): @@ -359,13 +360,13 @@ def test_databaseview_edit(self, username='admin'): # validate that sending a password-masked uri does not over-write the decrypted # uri self.login(username=username) - database = self.get_main_database(db.session) + database = get_main_database(db.session) sqlalchemy_uri_decrypted = database.sqlalchemy_uri_decrypted url = 'databaseview/edit/{}'.format(database.id) data = {k: database.__getattribute__(k) for k in DatabaseView.add_columns} data['sqlalchemy_uri'] = database.safe_sqlalchemy_uri() self.client.post(url, data=data) - database = self.get_main_database(db.session) + database = get_main_database(db.session) self.assertEqual(sqlalchemy_uri_decrypted, database.sqlalchemy_uri_decrypted) def test_warm_up_cache(self): @@ -452,27 +453,27 @@ def test_csv_endpoint(self): def test_extra_table_metadata(self): self.login('admin') - dbid = self.get_main_database(db.session).id + dbid = get_main_database(db.session).id self.get_json_resp( '/superset/extra_table_metadata/{dbid}/' 'ab_permission_view/panoramix/'.format(**locals())) def test_process_template(self): - maindb = self.get_main_database(db.session) + maindb = get_main_database(db.session) sql = "SELECT '{{ datetime(2017, 1, 1).isoformat() }}'" tp = jinja_context.get_template_processor(database=maindb) rendered = tp.process_template(sql) self.assertEqual("SELECT '2017-01-01T00:00:00'", rendered) def test_get_template_kwarg(self): - maindb = self.get_main_database(db.session) + maindb = get_main_database(db.session) s = '{{ foo }}' tp = jinja_context.get_template_processor(database=maindb, foo='bar') rendered = tp.process_template(s) self.assertEqual('bar', rendered) def test_template_kwarg(self): - maindb = self.get_main_database(db.session) + maindb = get_main_database(db.session) s = '{{ foo }}' tp = jinja_context.get_template_processor(database=maindb) rendered = tp.process_template(s, foo='bar') @@ -485,7 +486,7 @@ def test_templated_sql_json(self): self.assertEqual(data['data'][0]['test'], '2017-01-01T00:00:00') def test_table_metadata(self): - maindb = self.get_main_database(db.session) + maindb = get_main_database(db.session) backend = maindb.backend data = self.get_json_resp( '/superset/table/{}/ab_user/null/'.format(maindb.id)) diff --git a/tests/dict_import_export_tests.py b/tests/dict_import_export_tests.py index cbe8aa2ea240f..21a46ee9e8fcd 100644 --- a/tests/dict_import_export_tests.py +++ b/tests/dict_import_export_tests.py @@ -16,6 +16,7 @@ ) from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn from .base_tests import SupersetTestCase +from .utils import get_main_database DBREF = 'dict_import__export_test' NAME_PREFIX = 'dict_' @@ -55,7 +56,7 @@ def create_table( params = {DBREF: id, 'database_name': database_name} dict_rep = { - 'database_id': self.get_main_database(db.session).id, + 'database_id': get_main_database(db.session).id, 'table_name': name, 'schema': schema, 'id': id, diff --git a/tests/form_tests.py b/tests/form_tests.py index 82178a213eb09..93bef66bcdba4 100644 --- a/tests/form_tests.py +++ b/tests/form_tests.py @@ -4,11 +4,11 @@ from __future__ import print_function from __future__ import unicode_literals -from tests.base_tests import SupersetTestCase from wtforms.form import Form from superset.forms import ( CommaSeparatedListField, filter_not_empty_values) +from tests.base_tests import SupersetTestCase class FormTestCase(SupersetTestCase): diff --git a/tests/model_tests.py b/tests/model_tests.py index 74dc822645d00..565791a8e357f 100644 --- a/tests/model_tests.py +++ b/tests/model_tests.py @@ -11,6 +11,7 @@ from superset import app, db from superset.models.core import Database from .base_tests import SupersetTestCase +from .utils import get_main_database class DatabaseModelTestCase(SupersetTestCase): @@ -77,7 +78,7 @@ def test_database_impersonate_user(self): self.assertNotEquals(example_user, user_name) def test_select_star(self): - main_db = self.get_main_database(db.session) + main_db = get_main_database(db.session) table_name = 'bart_lines' sql = main_db.select_star( table_name, show_cols=False, latest_partition=False) @@ -107,7 +108,7 @@ def test_grains_dict(self): self.assertEquals(d.get('Time Column').function, '{col}') def test_single_statement(self): - main_db = self.get_main_database(db.session) + main_db = get_main_database(db.session) if main_db.backend == 'mysql': df = main_db.get_df('SELECT 1', None) @@ -117,7 +118,7 @@ def test_single_statement(self): self.assertEquals(df.iat[0, 0], 1) def test_multi_statement(self): - main_db = self.get_main_database(db.session) + main_db = get_main_database(db.session) if main_db.backend == 'mysql': df = main_db.get_df('USE superset; SELECT 1', None) diff --git a/tests/sqllab_tests.py b/tests/sqllab_tests.py index c3fd4045f7191..1159c4e8b3635 100644 --- a/tests/sqllab_tests.py +++ b/tests/sqllab_tests.py @@ -16,6 +16,7 @@ from superset.db_engine_specs import BaseEngineSpec from superset.models.sql_lab import Query from .base_tests import SupersetTestCase +from .utils import get_main_database class SqlLabTests(SupersetTestCase): @@ -62,7 +63,7 @@ def test_explain(self): self.assertLess(0, len(data['data'])) def test_sql_json_has_access(self): - main_db = self.get_main_database(db.session) + main_db = get_main_database(db.session) security_manager.add_permission_view_menu('database_access', main_db.perm) db.session.commit() main_db_permission_view = ( diff --git a/tests/utils.py b/tests/utils.py index d1a5adb01febb..a0ab45652295d 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -7,9 +7,20 @@ import json from os import path +from superset import db +from superset.models import core as models + FIXTURES_DIR = 'tests/fixtures' def load_fixture(fixture_file_name): with open(path.join(FIXTURES_DIR, fixture_file_name)) as fixture_file: return json.load(fixture_file) + + +def get_main_database(session): + return ( + db.session.query(models.Database) + .filter_by(database_name='main') + .first() + ) diff --git a/tox.ini b/tox.ini index b73d276841681..039075a8af8a3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,8 @@ [flake8] accept-encodings = utf-8 -application-import-names = superset +application-import-names = + superset + tests exclude = .tox build @@ -43,6 +45,17 @@ setenv = whitelist_externals = npm +[testenv:cypress] +commands = + {toxinidir}/superset/assets/cypress_build.sh +setenv = + PYTHONPATH = {toxinidir} + SUPERSET_CONFIG = tests.superset_test_config + SUPERSET_HOME = {envtmpdir} +deps = + -rrequirements.txt + -rrequirements-dev.txt + [testenv:eslint] changedir = {toxinidir}/superset/assets commands = @@ -70,6 +83,7 @@ deps = [tox] envlist = + cypress eslint flake8 javascript