├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .ruby-version ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib ├── tableau_api.rb └── tableau_api │ ├── client.rb │ ├── connection.rb │ ├── error.rb │ ├── resources │ ├── auth.rb │ ├── base.rb │ ├── datasources.rb │ ├── groups.rb │ ├── jobs.rb │ ├── projects.rb │ ├── sites.rb │ ├── users.rb │ └── workbooks.rb │ └── version.rb ├── spec ├── client_spec.rb ├── fixtures │ ├── vcr_cassettes │ │ ├── auth.yml │ │ ├── datasources.yml │ │ ├── groups.yml │ │ ├── jobs.yml │ │ ├── projects.yml │ │ ├── sites.yml │ │ ├── users.yml │ │ └── workbooks.yml │ └── workbooks │ │ └── test.twbx ├── resources │ ├── auth_spec.rb │ ├── datasources_spec.rb │ ├── groups_spec.rb │ ├── jobs_spec.rb │ ├── projects_spec.rb │ ├── sites_spec.rb │ ├── users_spec.rb │ └── workbooks_spec.rb ├── spec_helper.rb └── tableau_api_spec.rb └── tableau_api.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | AllCops: 4 | TargetRubyVersion: 2.7 5 | NewCops: enable 6 | 7 | Gemspec/RequiredRubyVersion: 8 | Enabled: false 9 | 10 | Layout/LineLength: 11 | Max: 170 12 | 13 | Style/Documentation: 14 | Enabled: false 15 | 16 | Metrics/ClassLength: 17 | Max: 200 18 | 19 | Metrics/AbcSize: 20 | Max: 40 21 | 22 | Metrics/MethodLength: 23 | Max: 30 24 | 25 | Metrics/BlockLength: 26 | Exclude: 27 | - ./tableau_api.gemspec 28 | - ./spec/resources/workbooks_spec.rb 29 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2019-08-21 17:54:45 +0000 using RuboCop version 0.49.1. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 13 10 | # Configuration parameters: CountComments, ExcludedMethods. 11 | Metrics/BlockLength: 12 | Max: 316 13 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.1 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | cache: bundler 2 | branches: 3 | only: 4 | - master 5 | language: ruby 6 | rvm: 7 | - 3.2.1 8 | - 3.1.3 9 | - 2.7.0 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## upcoming 7 | 8 | ### Changed 9 | 10 | ## [5.0.0] - 2023-03-23 11 | 12 | - Added support for Ruby 3 13 | - Bumped dev Ruby version to 3.2.1 14 | - Bumped minimum required Ruby version to 2.7.0 15 | - Bumped Rake version for development to 12.3.3 16 | - Bumped Rubocop version for development to 1.48.1 17 | - Bumped vcr version for development to 6.0 18 | - Bumped Travis matrix Ruby versions to 3.2.1, 3.0.5, and 2.7.0 19 | - Added Rubygems MFA requirement 20 | 21 | ## [4.1.0] - 2022-04-13 22 | 23 | - Specify major version number of ruby in Docker testing command in README 24 | - Add support for CreateRefreshMetrics and RunExplainData workbook capabilities 25 | 26 | ## [4.0.0] - 2020-11-30 27 | 28 | ### Changed/Fixed 29 | 30 | - Changed interface to Connection#api_get_collection to properly merge a string 31 | query with the pagination parameters instead of overwriting the pagination 32 | params. This means you can't pass a Hash `query` parameter to `Jobs#list` 33 | anymore, but job filtering now works properly because colons in the filter 34 | will not be URL-encoded. 35 | - Avoid mutating extra argument hash to endpoint methods 36 | 37 | 38 | ## [3.0.0] - 2020-11-11 39 | 40 | ### Added 41 | 42 | - Added Jobs resource 43 | 44 | 45 | ### Changed 46 | 47 | - Updated to API version 3.1 48 | - This is a breaking change for site roles: 49 | https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_concepts_new_site_roles.htm 50 | - Include full error detail in TableauError message 51 | 52 | 53 | ## [2.0.0] - 2019-08-29 54 | 55 | - Updated to API version 2.8, compatible with Tableau Server >= 10.5 56 | - Add `refresh` and `image_preview` methods for workbooks 57 | - Bumped the Ruby versions in the Travis matrix build to 2.3.4, 2.2.7, and 58 | 59 | ## [1.1.2] - 2017-05-24 60 | 61 | ### Fixed 62 | 63 | - Replaced corrupt `.gem` upload to RubyGems 64 | 65 | ## [1.1.1] - 2017-05-24 - [YANKED] 66 | 67 | ### Added 68 | 69 | - Added Ruby 2.4.1 to the Travis matrix build 70 | 71 | ### Changed 72 | 73 | - Bumped the Ruby versions in the Travis matrix build to 2.3.4, 2.2.7, and 74 | 2.1.10 75 | - Bumped the Ruby version for development to 2.4.1 76 | - Bumped the RSpec version for development to 3.6 77 | - Bumped the WebMock version for development to 3.0 78 | - Updated the authors 79 | 80 | ### Fixed 81 | 82 | - Updated `TableauApi::VERSION` 83 | 84 | ## [1.1.0] - 2017-03-02 85 | 86 | ### Added 87 | 88 | - [#2](https://github.com/civisanalytics/tableau_api/pull/2) 89 | - `TableauApi::Resources::Groups` added to support API calls for 90 | adding/deleting/updating groups. 91 | - `TableauApi::Resources::Workbook#remove_permissions` added, including 92 | support for user and group permissions. 93 | - `TableauApi::Resources::Workbook#add_permissions` supports group 94 | permissions. 95 | - [#6](https://github.com/civisanalytics/tableau_api/pull/6) 96 | Added `Users#update_user` 97 | - [#7](https://github.com/civisanalytics/tableau_api/pull/7) 98 | Added `Sites#create` and `Sites#delete` 99 | 100 | ### Changed 101 | 102 | - [#3](https://github.com/civisanalytics/tableau_api/pull/3) 103 | `TableauApi::Resources::Workbook#permissions` now returns existing permissions 104 | instead of adding new permissions. New permissions can be added with 105 | `TableauApi::Resources::Workbook#add_permissions`. 106 | 107 | ### Fixed 108 | 109 | - [#4](https://github.com/civisanalytics/tableau_api/pull/4) 110 | Always parse/return workbook permissions as an array 111 | 112 | ## [1.0.0] - 2016-06-06 113 | 114 | - Initial Release 115 | 116 | [Unreleased]: https://github.com/civisanalytics/tableau_api/compare/v1.1.2...HEAD 117 | [1.1.2]: https://github.com/civisanalytics/tableau_api/compare/v1.1.1...v1.1.2 118 | [1.1.1]: https://github.com/civisanalytics/tableau_api/compare/v1.1.0...v1.1.1 119 | [1.1.0]: https://github.com/civisanalytics/tableau_api/compare/v1.0.0...v1.1.0 120 | [1.0.0]: https://github.com/civisanalytics/tableau_api/tree/v1.0.0 121 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This code of conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at opensource@civisanalytics.com. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 45 | version 1.3.0, available at 46 | [http://contributor-covenant.org/version/1/3/0/][version] 47 | 48 | [homepage]: http://contributor-covenant.org 49 | [version]: http://contributor-covenant.org/version/1/3/0/ 50 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in tableau_api.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Civis Analytics 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of Civis Analytics nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tableau_api 2 | 3 | [![Build Status](https://travis-ci.org/civisanalytics/tableau_api.svg?branch=master)](https://travis-ci.org/civisanalytics/tableau_api) 4 | [![Gem Version](https://badge.fury.io/rb/tableau_api.svg)](http://badge.fury.io/rb/tableau_api) 5 | 6 | Ruby interface to the Tableau 9.0 API. 7 | 8 | ## Installation 9 | 10 | Add this line to your application's Gemfile: 11 | 12 | ```ruby 13 | gem 'tableau_api' 14 | ``` 15 | 16 | And then execute: 17 | 18 | $ bundle 19 | 20 | Or install it yourself as: 21 | 22 | $ gem install tableau_api 23 | 24 | ## Usage 25 | 26 | ### Basic Authentication 27 | ``` 28 | client = TableauApi.new(host: 'https://tableau.domain.tld', site_name: 'Default', username: 'ExampleUsername', password: 'ExamplePassword') 29 | client.users.create(username: 'baz') 30 | ``` 31 | 32 | ### Trusted Authentication 33 | ``` 34 | client = TableauApi.new(host: 'https://tableau.domain.tld', site_name: 'Default', username: 'ExampleUsername') 35 | client.auth.trusted_ticket 36 | ``` 37 | 38 | ### Workbooks 39 | ``` 40 | # find a workbook by name 41 | workbook = client.workbooks.list.find do |w| 42 | w['name'] == 'Example Workbook Name' 43 | end 44 | ``` 45 | 46 | ## Development 47 | 48 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run the tests and linters. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 49 | 50 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 51 | 52 | ## Testing 53 | 54 | ### Docker 55 | 56 | ``` 57 | docker run -it -d -v $(pwd):/src ruby:2 /bin/bash 58 | docker exec -it CONTAINER_ID /bin/bash -c "cd /src && bundle && rake" 59 | ``` 60 | 61 | ### Creating New VCR Cassettes 62 | 63 | Cassettes should be self-contained, generated by a single spec file 64 | run in defined order. To make changes to specs, it's best to delete a whole cassette 65 | and rerun the whole spec file, except for auth.yml, since it could be difficult to 66 | generate a trusted ticket from a non-trusted host. 67 | 68 | To regenerate all the the cassettes, you'll first need to create the following on the Tableau server: 69 | * *Site*: Default 70 | * *Site*: TestSite 71 | * *Datasource*: test (this might need to be created from Tableau Desktop) 72 | * *Username*: test_test 73 | 74 | And delete the following if they exist: 75 | * *Group*: testgroup (probably under TestSite) 76 | * *Project*: test_project (probably under Default) 77 | * *Site*: Test Site 2 78 | * *User*: test (probably under TestSite) 79 | * *Workbook*: test 80 | * *Workbook*: testpublish 81 | 82 | Set the environment variables below to an administrator account and your Tableau Server hostname. 83 | 84 | Then run the commands below: 85 | 86 | ``` 87 | docker run -it -d \ 88 | -v $(pwd):/src \ 89 | -e TABLEAU_HOST -e TABLEAU_ADMIN_USERNAME -e TABLEAU_ADMIN_PASSWORD \ 90 | ruby /bin/bash 91 | docker exec -it CONTAINER_ID /bin/bash -c "cd /src && bundle && rake" 92 | ``` 93 | 94 | ## Contributing 95 | 96 | Bug reports and pull requests are welcome on GitHub at https://github.com/civisanalytics/tableau_api. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 97 | 98 | ## License 99 | 100 | tableau_api is released under the [BSD 3-Clause License](LICENSE.txt). 101 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | require 'rubocop/rake_task' 6 | 7 | RuboCop::RakeTask.new(:rubocop) 8 | 9 | RSpec::Core::RakeTask.new(:spec) 10 | 11 | task default: %i[rubocop spec] 12 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'tableau_api' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | require 'pry' 11 | Pry.start 12 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/tableau_api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'httparty' 4 | require 'builder' 5 | require 'net/http/post/multipart' 6 | 7 | require 'tableau_api/version' 8 | 9 | require 'tableau_api/client' 10 | require 'tableau_api/connection' 11 | require 'tableau_api/error' 12 | require 'tableau_api/resources/base' 13 | require 'tableau_api/resources/auth' 14 | require 'tableau_api/resources/projects' 15 | require 'tableau_api/resources/sites' 16 | require 'tableau_api/resources/users' 17 | require 'tableau_api/resources/groups' 18 | require 'tableau_api/resources/workbooks' 19 | require 'tableau_api/resources/datasources' 20 | require 'tableau_api/resources/jobs' 21 | 22 | module TableauApi 23 | class << self 24 | def new(**options) 25 | Client.new(**options) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/tableau_api/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TableauApi 4 | class Client 5 | attr_reader :host, :username, :password, :site_id, :site_name 6 | 7 | def initialize(host:, site_name:, username:, password: nil) 8 | @resources = {} 9 | 10 | raise 'host is required' if host.to_s.empty? 11 | 12 | @host = host 13 | 14 | raise 'site_name is required' if site_name.to_s.empty? 15 | 16 | @site_name = site_name 17 | 18 | raise 'username is required' if username.to_s.empty? 19 | 20 | @username = username 21 | 22 | @password = password 23 | end 24 | 25 | def connection 26 | @connection ||= Connection.new(self) 27 | end 28 | 29 | def self.resources 30 | { 31 | auth: TableauApi::Resources::Auth, 32 | projects: TableauApi::Resources::Projects, 33 | sites: TableauApi::Resources::Sites, 34 | users: TableauApi::Resources::Users, 35 | groups: TableauApi::Resources::Groups, 36 | workbooks: TableauApi::Resources::Workbooks, 37 | datasources: TableauApi::Resources::Datasources, 38 | jobs: TableauApi::Resources::Jobs 39 | } 40 | end 41 | 42 | def method_missing(name, *args, &block) 43 | if self.class.resources.keys.include?(name) 44 | @resources[name] ||= self.class.resources[name].new(self) 45 | @resources[name] 46 | else 47 | super 48 | end 49 | end 50 | 51 | def respond_to_missing?(name, include_private = false) 52 | self.class.resources.keys.include?(name) || super 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/tableau_api/connection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TableauApi 4 | class Connection 5 | API_VERSION = '3.1' 6 | 7 | include HTTParty 8 | headers 'User-Agent' => "tableau_api/#{::TableauApi::VERSION} Ruby/#{RUBY_VERSION}" 9 | 10 | def initialize(client) 11 | @client = client 12 | end 13 | 14 | def post(path, **kwargs) 15 | self.class.post("#{@client.host}/#{path}", kwargs) 16 | end 17 | 18 | # if the result is paginated, it will fetch subsequent pages 19 | # collection can be delimited with a period to do nested hash lookups 20 | # e.g. objects.object 21 | # rubocop:disable Metrics/CyclomaticComplexity 22 | def api_get_collection(path, collection, page_number: 1, page_size: 100, **kwargs) 23 | Enumerator.new do |enum| 24 | loop do 25 | query = kwargs.fetch(:query, '') 26 | query += '&' unless query.empty? 27 | query += "pageSize=#{page_size}&pageNumber=#{page_number}" 28 | new_kwargs = kwargs.merge(query: query) 29 | 30 | res = api_get(path, **new_kwargs) 31 | raise TableauError, res if res.code.to_s != '200' 32 | 33 | # ensure the result is an array because it will not be an array if there is only one element 34 | [collection.split('.').reduce(res['tsResponse']) { |acc, elem| acc && acc[elem] }].flatten.compact.each do |obj| 35 | enum.yield obj 36 | end 37 | 38 | break if res['tsResponse']['pagination'].nil? 39 | break if page_number >= (res['tsResponse']['pagination']['totalAvailable'].to_i / page_size.to_f).ceil 40 | 41 | page_number += 1 42 | end 43 | end 44 | end 45 | # rubocop:enable Metrics/CyclomaticComplexity 46 | 47 | def api_get(path, **kwargs) 48 | api_method(:get, path, kwargs) 49 | end 50 | 51 | def api_post(path, **kwargs) 52 | new_headers = kwargs.fetch(:headers, {}).merge('Content-Type' => 'application/xml') 53 | api_method(:post, path, kwargs.merge(headers: new_headers)) 54 | end 55 | 56 | def api_put(path, **kwargs) 57 | new_headers = kwargs.fetch(:headers, {}).merge('Content-Type' => 'application/xml') 58 | api_method(:put, path, kwargs.merge(headers: new_headers)) 59 | end 60 | 61 | def api_delete(path, **kwargs) 62 | api_method(:delete, path, kwargs) 63 | end 64 | 65 | def api_post_multipart(path, parts, headers) 66 | headers = auth_headers(headers) 67 | headers['Content-Type'] = 'multipart/mixed' 68 | 69 | uri = URI.parse(url_for(path)) 70 | 71 | req = Net::HTTP::Post::Multipart.new(uri.to_s, parts, headers) 72 | 73 | Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http| 74 | http.request(req) 75 | end 76 | end 77 | 78 | private 79 | 80 | def api_method(method, path, kwargs) 81 | # do not attach auth headers or attempt to signin if we're signing in 82 | new_headers = auth_headers(kwargs.fetch(:headers, {})) unless path == 'auth/signin' 83 | self.class.public_send(method, url_for(path), kwargs.merge(headers: new_headers)) 84 | end 85 | 86 | def url_for(path) 87 | "#{@client.host}/api/#{API_VERSION}#{'/' unless path[0] == '/'}#{path}" 88 | end 89 | 90 | # will attempt to signin if the token hasn't been loaded 91 | def auth_headers(headers = {}) 92 | h = headers.dup 93 | h['X-Tableau-Auth'] = @client.auth.token 94 | h 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/tableau_api/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TableauApi 4 | class TableauError < StandardError 5 | attr_reader :http_response_code, :error_code, :summary, :detail 6 | 7 | def initialize(net_response) 8 | @http_response_code = net_response.code 9 | error = HTTParty::Parser.new(net_response.body, :xml).parse['tsResponse']['error'] 10 | @error_code = error['code'] 11 | @summary = error['summary'] 12 | @detail = error['detail'] 13 | super("#{error_code}: #{summary}; #{detail}") 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/tableau_api/resources/auth.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TableauApi 4 | module Resources 5 | class Auth < Base 6 | def token 7 | sign_in unless signed_in? 8 | @token 9 | end 10 | 11 | def site_id 12 | sign_in unless signed_in? 13 | @site_id 14 | end 15 | 16 | def user_id 17 | sign_in unless signed_in? 18 | @user_id 19 | end 20 | 21 | def sign_in 22 | return true if signed_in? 23 | 24 | request = Builder::XmlMarkup.new.tsRequest do |ts| 25 | ts.credentials(name: @client.username, password: @client.password) do |cred| 26 | cred.site(contentUrl: @client.site_name == 'Default' ? '' : @client.site_name) 27 | end 28 | end 29 | 30 | res = @client.connection.api_post('auth/signin', body: request) 31 | 32 | return false unless res.code == 200 33 | 34 | @token = res['tsResponse']['credentials']['token'] 35 | @site_id = res['tsResponse']['credentials']['site']['id'] 36 | @user_id = res['tsResponse']['credentials']['user']['id'] 37 | 38 | true 39 | end 40 | 41 | def signed_in? 42 | !@token.nil? 43 | end 44 | 45 | def sign_out 46 | return true unless signed_in? 47 | 48 | res = @client.connection.api_post('auth/signout', body: nil) 49 | 50 | # consider 401 to be successful since signing out with an expired 51 | # token fails, but we can still consider the user signed out 52 | return false unless res.code == 204 || res.code == 401 53 | 54 | @token = nil 55 | @site_id = nil 56 | @user_id = nil 57 | 58 | true 59 | end 60 | 61 | def trusted_ticket 62 | body = { 63 | username: @client.username, 64 | target_site: @client.site_name == 'Default' ? '' : @client.site_name 65 | } 66 | 67 | begin 68 | res = @client.connection.post('trusted', body: body, limit: 1) 69 | rescue HTTParty::RedirectionTooDeep 70 | # redirects if the site_name is invalid 71 | res = false 72 | end 73 | 74 | return unless res && res.code == 200 && res.body != '-1' 75 | 76 | res.body 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/tableau_api/resources/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TableauApi 4 | module Resources 5 | class Base 6 | def initialize(client) 7 | @client = client 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/tableau_api/resources/datasources.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TableauApi 4 | module Resources 5 | class Datasources < Base 6 | def list 7 | url = "sites/#{@client.auth.site_id}/datasources" 8 | @client.connection.api_get_collection(url, 'datasources.datasource') 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/tableau_api/resources/groups.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TableauApi 4 | module Resources 5 | class Groups < Base 6 | def create(name:, default_site_role: 'Viewer') 7 | raise 'invalid default_site_role' unless Users::SITE_ROLES.include? default_site_role 8 | 9 | request = Builder::XmlMarkup.new.tsRequest do |ts| 10 | ts.group(name: name, defaultSiteRole: default_site_role) 11 | end 12 | 13 | res = @client.connection.api_post("sites/#{@client.auth.site_id}/groups", body: request) 14 | 15 | res['tsResponse']['group'] if res.code == 201 16 | end 17 | 18 | def list 19 | url = "sites/#{@client.auth.site_id}/groups" 20 | @client.connection.api_get_collection(url, 'groups.group') 21 | end 22 | 23 | def users(group_id:) 24 | url = "sites/#{@client.auth.site_id}/groups/#{group_id}/users" 25 | @client.connection.api_get_collection(url, 'users.user') 26 | end 27 | 28 | def add_user(group_id:, user_id:) 29 | request = Builder::XmlMarkup.new.tsRequest do |ts| 30 | ts.user(id: user_id) 31 | end 32 | 33 | res = @client.connection.api_post("sites/#{@client.auth.site_id}/groups/#{group_id}/users", body: request) 34 | 35 | res.code == 200 36 | end 37 | 38 | def remove_user(group_id:, user_id:) 39 | res = @client.connection.api_delete("sites/#{@client.auth.site_id}/groups/#{group_id}/users/#{user_id}") 40 | 41 | res.code == 204 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/tableau_api/resources/jobs.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TableauApi 4 | module Resources 5 | class Jobs < Base 6 | def list(params = {}) 7 | url = "sites/#{@client.auth.site_id}/jobs" 8 | @client.connection.api_get_collection(url, 'backgroundJobs.backgroundJob', **params) 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/tableau_api/resources/projects.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TableauApi 4 | module Resources 5 | class Projects < Base 6 | def create(name:, description: '') 7 | request = Builder::XmlMarkup.new.tsRequest do |ts| 8 | ts.project(name: name, description: description) 9 | end 10 | 11 | res = @client.connection.api_post("sites/#{@client.auth.site_id}/projects", body: request) 12 | 13 | res['tsResponse']['project'] if res.code == 201 14 | end 15 | 16 | def list 17 | url = "sites/#{@client.auth.site_id}/projects" 18 | @client.connection.api_get_collection(url, 'projects.project') 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/tableau_api/resources/sites.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TableauApi 4 | module Resources 5 | class Sites < Base 6 | def list 7 | @client.connection.api_get_collection('sites', 'sites.site') 8 | end 9 | 10 | def create(name:, content_url:, admin_mode: nil, num_users: nil, storage_quota: nil) 11 | # required parameters 12 | request_hash = { 13 | name: name, 14 | contentUrl: content_url 15 | } 16 | # optional parameters 17 | request_hash[:admin_mode] = admin_mode if admin_mode 18 | request_hash[:num_users] = num_users if num_users 19 | request_hash[:storage_quota] = storage_quota if storage_quota 20 | 21 | request = Builder::XmlMarkup.new.tsRequest do |ts| 22 | ts.site(request_hash) 23 | end 24 | 25 | res = @client.connection.api_post('sites', body: request) 26 | 27 | return res['tsResponse']['site'] if res.code == 201 28 | 29 | raise TableauError, res 30 | end 31 | 32 | def delete(site_id:) 33 | res = @client.connection.api_delete("sites/#{site_id}") 34 | return true if res.code == 204 35 | 36 | raise TableauError, res 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/tableau_api/resources/users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TableauApi 4 | module Resources 5 | class Users < Base 6 | SITE_ROLES = %w[ 7 | Creator 8 | Explorer 9 | ExplorerCanPublish 10 | SiteAdministratorCreator 11 | SiteAdministratorExplorer 12 | ServerAdministrator 13 | Unlicensed 14 | Viewer 15 | ].freeze 16 | 17 | def create(username:, site_role: 'Viewer') 18 | raise 'invalid site_role' unless SITE_ROLES.include? site_role 19 | 20 | request = Builder::XmlMarkup.new.tsRequest do |ts| 21 | ts.user(name: username, siteRole: site_role) 22 | end 23 | 24 | res = @client.connection.api_post("sites/#{@client.auth.site_id}/users", body: request) 25 | 26 | res['tsResponse']['user'] if res.code == 201 27 | end 28 | 29 | def list 30 | url = "sites/#{@client.auth.site_id}/users" 31 | @client.connection.api_get_collection(url, 'users.user') 32 | end 33 | 34 | def update_user(user_id:, site_role:) 35 | raise 'invalid site_role' unless SITE_ROLES.include? site_role 36 | 37 | res = @client.connection.api_get("sites/#{@client.auth.site_id}/users/#{user_id}") 38 | 39 | raise 'failed to find user' if res.code != 200 40 | 41 | user = res['tsResponse']['user'] 42 | 43 | request = Builder::XmlMarkup.new.tsRequest do |ts| 44 | ts.user(name: user['name'], siteRole: site_role) 45 | end 46 | 47 | res = @client.connection.api_put("sites/#{@client.auth.site_id}/users/#{user_id}", body: request) 48 | 49 | res['tsResponse']['user'] if res.code == 200 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/tableau_api/resources/workbooks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'zip' 4 | 5 | module TableauApi 6 | module Resources 7 | class Workbooks < Base 8 | def version(file) 9 | version = nil 10 | 11 | if File.exist?(file) && File.extname(file) == '.twbx' 12 | Zip::File.open(file) do |zip_file| 13 | entry = zip_file.glob('*.twb').first 14 | version = HTTParty::Parser.new(entry.get_input_stream.read, :xml).parse['workbook']['version'] 15 | end 16 | end 17 | 18 | version 19 | end 20 | 21 | # rubocop:disable Metrics/ParameterLists 22 | def publish(name:, project_id:, file:, overwrite: false, show_tabs: false, connection_username: nil, connection_password: nil, connection_embed: false) 23 | request = Builder::XmlMarkup.new.tsRequest do |ts| 24 | ts.workbook(name: name, showTabs: show_tabs) do |wb| 25 | wb.project(id: project_id) 26 | wb.connectionCredentials(name: connection_username, password: connection_password, embed: connection_embed) if connection_username 27 | end 28 | end 29 | 30 | query = URI.encode_www_form([['overwrite', overwrite]]) 31 | path = "sites/#{@client.auth.site_id}/workbooks?#{query}" 32 | 33 | parts = { 34 | 'request_payload' => request, 35 | 'tableau_workbook' => UploadIO.new(file, 'application/octet-stream') 36 | } 37 | 38 | headers = { 39 | parts: { 40 | 'request_payload' => { 'Content-Type' => 'text/xml' }, 41 | 'tableau_workbook' => { 'Content-Type' => 'application/octet-string' } 42 | } 43 | } 44 | 45 | res = @client.connection.api_post_multipart(path, parts, headers) 46 | 47 | return HTTParty::Parser.new(res.body, :xml).parse['tsResponse']['workbook'] if res.code == '201' 48 | 49 | raise TableauError, res 50 | end 51 | # rubocop:enable Metrics/ParameterLists 52 | 53 | CAPABILITIES = %w[ 54 | AddComment ChangeHierarchy ChangePermissions CreateRefreshMetrics Delete ExportData ExportImage 55 | ExportXml Filter Read RunExplainData ShareView ViewComments ViewUnderlyingData WebAuthoring Write 56 | ].freeze 57 | 58 | CAPABILITY_MODES = %w[ALLOW DENY].freeze 59 | 60 | # rubocop:disable Metrics/CyclomaticComplexity 61 | def permissions(workbook_id:) 62 | res = @client.connection.api_get("sites/#{@client.auth.site_id}/workbooks/#{workbook_id}/permissions") 63 | 64 | raise TableauError, res if res.code != 200 65 | 66 | permissions = HTTParty::Parser.new(res.body, :xml).parse['tsResponse']['permissions']['granteeCapabilities'] 67 | return [] if permissions.nil? 68 | 69 | permissions = [permissions] unless permissions.is_a? Array 70 | permissions.map do |p| 71 | grantee_type = p['group'].nil? ? 'user' : 'group' 72 | 73 | capabilities = {} 74 | capabilities_list = p['capabilities']['capability'] 75 | capabilities_list = [capabilities_list] unless capabilities_list.is_a? Array 76 | 77 | capabilities_list.each do |c| 78 | capabilities[c['name'].to_sym] = c['mode'] == 'Allow' 79 | end 80 | 81 | { 82 | grantee_type: grantee_type, 83 | grantee_id: p[grantee_type]['id'], 84 | capabilities: capabilities 85 | } 86 | end 87 | end 88 | # rubocop:enable Metrics/CyclomaticComplexity 89 | 90 | # capabilities is a hash of symbol keys to booleans { Read: true, ChangePermissions: false } 91 | def add_permissions(workbook_id:, capabilities:, user_id: nil, group_id: nil) 92 | validate_user_group_exclusivity(user_id, group_id) 93 | 94 | request = permissions_request(workbook_id, user_id, group_id, capabilities) 95 | res = @client.connection.api_put("sites/#{@client.auth.site_id}/workbooks/#{workbook_id}/permissions", body: request) 96 | 97 | res.code == 200 98 | end 99 | 100 | def delete_permissions(workbook_id:, capability:, capability_mode:, user_id: nil, group_id: nil) 101 | validate_user_group_exclusivity(user_id, group_id) 102 | raise 'invalid capability' unless CAPABILITIES.include? capability.to_s 103 | raise 'invalid mode' unless CAPABILITY_MODES.include? capability_mode.to_s 104 | 105 | subpath = user_id ? "users/#{user_id}" : "groups/#{group_id}" 106 | subpath += "/#{capability}/#{capability_mode.capitalize}" 107 | res = @client.connection.api_delete("sites/#{@client.auth.site_id}/workbooks/#{workbook_id}/permissions/#{subpath}") 108 | 109 | res.code == 204 110 | end 111 | 112 | def update(workbook_id:, owner_user_id:) 113 | request = Builder::XmlMarkup.new.tsRequest do |ts| 114 | ts.workbook(id: workbook_id) do |w| 115 | w.owner(id: owner_user_id) 116 | end 117 | end 118 | 119 | res = @client.connection.api_put("sites/#{@client.auth.site_id}/workbooks/#{workbook_id}", body: request) 120 | 121 | res.code == 200 122 | end 123 | 124 | def refresh(workbook_id:) 125 | request = Builder::XmlMarkup.new.tsRequest 126 | res = @client.connection.api_post("sites/#{@client.auth.site_id}/workbooks/#{workbook_id}/refresh", body: request) 127 | res.code == 202 128 | end 129 | 130 | def preview_image(workbook_id:) 131 | res = @client.connection.api_get("sites/#{@client.auth.site_id}/workbooks/#{workbook_id}/previewImage") 132 | res.body if res.code == 200 133 | end 134 | 135 | def find(workbook_id) 136 | res = @client.connection.api_get("sites/#{@client.auth.site_id}/workbooks/#{workbook_id}") 137 | res['tsResponse']['workbook'] if res.code == 200 138 | end 139 | 140 | def views(workbook_id) 141 | url = "sites/#{@client.auth.site_id}/workbooks/#{workbook_id}/views" 142 | @client.connection.api_get_collection(url, 'views.view') 143 | end 144 | 145 | def list 146 | url = "sites/#{@client.auth.site_id}/users/#{@client.auth.user_id}/workbooks" 147 | @client.connection.api_get_collection(url, 'workbooks.workbook') 148 | end 149 | 150 | private 151 | 152 | def permissions_request(workbook_id, user_id, group_id, capabilities) 153 | Builder::XmlMarkup.new.tsRequest do |ts| 154 | ts.permissions do |p| 155 | p.workbook(id: workbook_id) 156 | p.granteeCapabilities do |gc| 157 | gc.user(id: user_id) if user_id 158 | gc.group(id: group_id) if group_id 159 | gc.capabilities do |c| 160 | capabilities.each do |k, v| 161 | k = k.to_s 162 | raise "invalid capability #{k}" unless CAPABILITIES.include? k 163 | 164 | c.capability(name: k, mode: v ? 'Allow' : 'Deny') 165 | end 166 | end 167 | end 168 | end 169 | end 170 | end 171 | 172 | def validate_user_group_exclusivity(user_id, group_id) 173 | raise 'cannot specify user_id and group_id simultaneously' if user_id && group_id 174 | raise 'must specify user_id or group_id' unless user_id || group_id 175 | end 176 | end 177 | end 178 | end 179 | -------------------------------------------------------------------------------- /lib/tableau_api/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TableauApi 4 | VERSION = '5.0.0' 5 | end 6 | -------------------------------------------------------------------------------- /spec/client_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe TableauApi::Client do 6 | it 'can create a client without issuing any web requests' do 7 | # this would exception if this made any http requests because VCR is set to not allow http connections when no cassette is loaded 8 | client = TableauApi.new(host: 'tableau.domain.tld', site_name: 'Default', username: 'ExampleUsername', password: 'ExamplePassword') 9 | expect(client).to be_an_instance_of(TableauApi::Client) 10 | end 11 | 12 | it 'requires the host, site_name, and username' do 13 | expect { TableauApi.new(host: nil, site_name: 'bar', username: 'baz') }.to raise_error('host is required') 14 | expect { TableauApi.new(host: '', site_name: 'bar', username: 'baz') }.to raise_error('host is required') 15 | 16 | expect { TableauApi.new(host: 'foo', site_name: nil, username: 'baz') }.to raise_error('site_name is required') 17 | expect { TableauApi.new(host: 'foo', site_name: '', username: 'baz') }.to raise_error('site_name is required') 18 | 19 | expect { TableauApi.new(host: 'foo', site_name: 'bar', username: nil) }.to raise_error('username is required') 20 | expect { TableauApi.new(host: 'foo', site_name: 'bar', username: '') }.to raise_error('username is required') 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/auth.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: http://TABLEAU_HOST/trusted 6 | body: 7 | encoding: UTF-8 8 | string: username=test&target_site= 9 | headers: 10 | Accept-Encoding: 11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 12 | Accept: 13 | - "*/*" 14 | User-Agent: 15 | - Ruby 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - private, max-age=0, must-revalidate 23 | Content-Security-Policy-Report-Only: 24 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 25 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 26 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 27 | Content-Type: 28 | - text/plain;charset=UTF-8 29 | Date: 30 | - Mon, 26 Aug 2019 19:38:04 GMT 31 | P3p: 32 | - CP="NON" 33 | Pragma: 34 | - '' 35 | Server: 36 | - Tableau 37 | X-Content-Type-Options: 38 | - nosniff 39 | X-Tableau: 40 | - Tableau Server 41 | X-Tsi-Request-Id: 42 | - XWQ1HO1OS0ZGKhqUWLZFSAAAAAM 43 | X-Ua-Compatible: 44 | - IE=Edge 45 | X-Xss-Protection: 46 | - 1; mode=block 47 | Content-Length: 48 | - '2' 49 | Connection: 50 | - keep-alive 51 | body: 52 | encoding: UTF-8 53 | string: ZXN-jGH7QYSdctK-K4cSWQ==:QTpusOYkzCrotcubBNo7htxy 54 | http_version: 55 | recorded_at: Mon, 26 Aug 2019 19:38:04 GMT 56 | - request: 57 | method: post 58 | uri: http://TABLEAU_HOST/trusted 59 | body: 60 | encoding: UTF-8 61 | string: username=test_test&target_site=test 62 | headers: 63 | Accept-Encoding: 64 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 65 | Accept: 66 | - "*/*" 67 | User-Agent: 68 | - Ruby 69 | response: 70 | status: 71 | code: 200 72 | message: OK 73 | headers: 74 | Cache-Control: 75 | - private, max-age=0, must-revalidate 76 | Content-Security-Policy-Report-Only: 77 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 78 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 79 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 80 | Content-Type: 81 | - text/plain;charset=UTF-8 82 | Date: 83 | - Mon, 26 Aug 2019 19:38:04 GMT 84 | P3p: 85 | - CP="NON" 86 | Pragma: 87 | - '' 88 | Server: 89 | - Tableau 90 | X-Content-Type-Options: 91 | - nosniff 92 | X-Tableau: 93 | - Tableau Server 94 | X-Tsi-Request-Id: 95 | - XWQ1HO1OS0ZGKhqUWLZFSQAAAB0 96 | X-Ua-Compatible: 97 | - IE=Edge 98 | X-Xss-Protection: 99 | - 1; mode=block 100 | Content-Length: 101 | - '2' 102 | Connection: 103 | - keep-alive 104 | body: 105 | encoding: UTF-8 106 | string: ZXN-jGH7QYSdctK-K4cSWQ==:QTpusOYkzCrotcubBNo7htxy 107 | http_version: 108 | recorded_at: Mon, 26 Aug 2019 19:38:04 GMT 109 | - request: 110 | method: post 111 | uri: http://TABLEAU_HOST/trusted 112 | body: 113 | encoding: UTF-8 114 | string: username=test&target_site=test 115 | headers: 116 | Accept-Encoding: 117 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 118 | Accept: 119 | - "*/*" 120 | User-Agent: 121 | - Ruby 122 | response: 123 | status: 124 | code: 200 125 | message: OK 126 | headers: 127 | Cache-Control: 128 | - private, max-age=0, must-revalidate 129 | Content-Security-Policy-Report-Only: 130 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 131 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 132 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 133 | Content-Type: 134 | - text/plain;charset=UTF-8 135 | Date: 136 | - Mon, 26 Aug 2019 19:38:04 GMT 137 | P3p: 138 | - CP="NON" 139 | Pragma: 140 | - '' 141 | Server: 142 | - Tableau 143 | X-Content-Type-Options: 144 | - nosniff 145 | X-Tableau: 146 | - Tableau Server 147 | X-Tsi-Request-Id: 148 | - XWQ1HRAYKqeRms0l4B9FSgAAAJE 149 | X-Ua-Compatible: 150 | - IE=Edge 151 | X-Xss-Protection: 152 | - 1; mode=block 153 | Content-Length: 154 | - '2' 155 | Connection: 156 | - keep-alive 157 | body: 158 | encoding: UTF-8 159 | string: "-1" 160 | http_version: 161 | recorded_at: Mon, 26 Aug 2019 19:38:05 GMT 162 | - request: 163 | method: post 164 | uri: http://TABLEAU_HOST/trusted 165 | body: 166 | encoding: UTF-8 167 | string: username=invalid_user&target_site= 168 | headers: 169 | Accept-Encoding: 170 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 171 | Accept: 172 | - "*/*" 173 | User-Agent: 174 | - Ruby 175 | response: 176 | status: 177 | code: 200 178 | message: OK 179 | headers: 180 | Cache-Control: 181 | - private, max-age=0, must-revalidate 182 | Content-Security-Policy-Report-Only: 183 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 184 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 185 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 186 | Content-Type: 187 | - text/plain;charset=UTF-8 188 | Date: 189 | - Mon, 26 Aug 2019 19:38:04 GMT 190 | P3p: 191 | - CP="NON" 192 | Pragma: 193 | - '' 194 | Server: 195 | - Tableau 196 | X-Content-Type-Options: 197 | - nosniff 198 | X-Tableau: 199 | - Tableau Server 200 | X-Tsi-Request-Id: 201 | - XWQ1He1OS0ZGKhqUWLZFSgAAAD8 202 | X-Ua-Compatible: 203 | - IE=Edge 204 | X-Xss-Protection: 205 | - 1; mode=block 206 | Content-Length: 207 | - '2' 208 | Connection: 209 | - keep-alive 210 | body: 211 | encoding: UTF-8 212 | string: "-1" 213 | http_version: 214 | recorded_at: Mon, 26 Aug 2019 19:38:05 GMT 215 | - request: 216 | method: post 217 | uri: http://TABLEAU_HOST/trusted 218 | body: 219 | encoding: UTF-8 220 | string: username=test&target_site=invalid_site 221 | headers: 222 | Accept-Encoding: 223 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 224 | Accept: 225 | - "*/*" 226 | User-Agent: 227 | - Ruby 228 | response: 229 | status: 230 | code: 302 231 | message: Found 232 | headers: 233 | Cache-Control: 234 | - no-store 235 | Content-Security-Policy-Report-Only: 236 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 237 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 238 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 239 | Date: 240 | - Mon, 26 Aug 2019 19:38:04 GMT 241 | Location: 242 | - http://TABLEAU_HOST/vizportal/api/web/v1/auth/signin?path=%2Ftrusted%3F&siteUrlName= 243 | P3p: 244 | - CP="NON" 245 | Server: 246 | - Tableau 247 | X-Content-Type-Options: 248 | - nosniff 249 | X-Tableau: 250 | - Tableau Server 251 | X-Tsi-Request-Id: 252 | - XWQ1He1OS0ZGKhqUWLZFSwAAADg 253 | X-Ua-Compatible: 254 | - IE=Edge 255 | X-Xss-Protection: 256 | - 1; mode=block 257 | Content-Length: 258 | - '0' 259 | Connection: 260 | - keep-alive 261 | body: 262 | encoding: UTF-8 263 | string: '' 264 | http_version: 265 | recorded_at: Mon, 26 Aug 2019 19:38:05 GMT 266 | - request: 267 | method: post 268 | uri: http://TABLEAU_HOST/api/3.1/auth/signin 269 | body: 270 | encoding: UTF-8 271 | string: 272 | headers: 273 | Content-Type: 274 | - application/xml 275 | Accept-Encoding: 276 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 277 | Accept: 278 | - "*/*" 279 | User-Agent: 280 | - Ruby 281 | response: 282 | status: 283 | code: 401 284 | message: Unauthorized 285 | headers: 286 | Content-Security-Policy-Report-Only: 287 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 288 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 289 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 290 | Content-Type: 291 | - application/xml;charset=utf-8 292 | Date: 293 | - Tue, 24 Nov 2020 21:59:25 GMT 294 | P3p: 295 | - CP="NON" 296 | Server: 297 | - Tableau 298 | X-Content-Type-Options: 299 | - nosniff 300 | X-Tableau: 301 | - Tableau Server 302 | X-Ua-Compatible: 303 | - IE=Edge 304 | X-Xss-Protection: 305 | - 1; mode=block 306 | Content-Length: 307 | - '341' 308 | Connection: 309 | - keep-alive 310 | body: 311 | encoding: UTF-8 312 | string: Signin 315 | ErrorError signing in to Tableau Server 316 | http_version: 317 | recorded_at: Tue, 24 Nov 2020 21:59:25 GMT 318 | - request: 319 | method: post 320 | uri: http://TABLEAU_HOST/api/3.1/auth/signin 321 | body: 322 | encoding: UTF-8 323 | string: 325 | headers: 326 | Content-Type: 327 | - application/xml 328 | Accept-Encoding: 329 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 330 | Accept: 331 | - "*/*" 332 | User-Agent: 333 | - Ruby 334 | response: 335 | status: 336 | code: 200 337 | message: OK 338 | headers: 339 | Content-Security-Policy-Report-Only: 340 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 341 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 342 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 343 | Content-Type: 344 | - application/xml;charset=utf-8 345 | Date: 346 | - Tue, 24 Nov 2020 21:59:25 GMT 347 | P3p: 348 | - CP="NON" 349 | Server: 350 | - Tableau 351 | Vary: 352 | - Accept-Encoding 353 | X-Content-Type-Options: 354 | - nosniff 355 | X-Tableau: 356 | - Tableau Server 357 | X-Ua-Compatible: 358 | - IE=Edge 359 | X-Xss-Protection: 360 | - 1; mode=block 361 | Transfer-Encoding: 362 | - chunked 363 | Connection: 364 | - keep-alive 365 | body: 366 | encoding: ASCII-8BIT 367 | string: 371 | http_version: 372 | recorded_at: Tue, 24 Nov 2020 21:59:26 GMT 373 | - request: 374 | method: post 375 | uri: http://TABLEAU_HOST/api/3.1/auth/signin 376 | body: 377 | encoding: UTF-8 378 | string: 380 | headers: 381 | Content-Type: 382 | - application/xml 383 | Accept-Encoding: 384 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 385 | Accept: 386 | - "*/*" 387 | User-Agent: 388 | - Ruby 389 | response: 390 | status: 391 | code: 200 392 | message: OK 393 | headers: 394 | Content-Security-Policy-Report-Only: 395 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 396 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 397 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 398 | Content-Type: 399 | - application/xml;charset=utf-8 400 | Date: 401 | - Tue, 24 Nov 2020 21:59:26 GMT 402 | P3p: 403 | - CP="NON" 404 | Server: 405 | - Tableau 406 | Vary: 407 | - Accept-Encoding 408 | X-Content-Type-Options: 409 | - nosniff 410 | X-Tableau: 411 | - Tableau Server 412 | X-Ua-Compatible: 413 | - IE=Edge 414 | X-Xss-Protection: 415 | - 1; mode=block 416 | Transfer-Encoding: 417 | - chunked 418 | Connection: 419 | - keep-alive 420 | body: 421 | encoding: ASCII-8BIT 422 | string: 426 | http_version: 427 | recorded_at: Tue, 24 Nov 2020 21:59:26 GMT 428 | - request: 429 | method: post 430 | uri: http://TABLEAU_HOST/api/3.1/auth/signout 431 | body: 432 | encoding: UTF-8 433 | string: '' 434 | headers: 435 | Content-Type: 436 | - application/xml 437 | X-Tableau-Auth: 438 | - lyRRiXUQTpqu44g7D79CsQ|dnKJm45x9iBtVokNOKBhAZ7aHKb2bBvC 439 | Accept-Encoding: 440 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 441 | Accept: 442 | - "*/*" 443 | User-Agent: 444 | - Ruby 445 | response: 446 | status: 447 | code: 204 448 | message: No Content 449 | headers: 450 | Content-Security-Policy-Report-Only: 451 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 452 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 453 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 454 | Date: 455 | - Tue, 24 Nov 2020 21:59:26 GMT 456 | P3p: 457 | - CP="NON" 458 | Server: 459 | - Tableau 460 | X-Content-Type-Options: 461 | - nosniff 462 | X-Tableau: 463 | - Tableau Server 464 | X-Ua-Compatible: 465 | - IE=Edge 466 | X-Xss-Protection: 467 | - 1; mode=block 468 | Connection: 469 | - keep-alive 470 | body: 471 | encoding: UTF-8 472 | string: '' 473 | http_version: 474 | recorded_at: Tue, 24 Nov 2020 21:59:26 GMT 475 | recorded_with: VCR 3.0.3 476 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/datasources.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: http://TABLEAU_HOST/api/3.1/auth/signin 6 | body: 7 | encoding: UTF-8 8 | string: 10 | headers: 11 | Content-Type: 12 | - application/xml 13 | Accept-Encoding: 14 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 15 | Accept: 16 | - "*/*" 17 | User-Agent: 18 | - Ruby 19 | response: 20 | status: 21 | code: 200 22 | message: OK 23 | headers: 24 | Content-Security-Policy-Report-Only: 25 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 26 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 27 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 28 | Content-Type: 29 | - application/xml;charset=utf-8 30 | Date: 31 | - Wed, 28 Oct 2020 20:40:16 GMT 32 | P3p: 33 | - CP="NON" 34 | Server: 35 | - Tableau 36 | Vary: 37 | - Accept-Encoding 38 | X-Content-Type-Options: 39 | - nosniff 40 | X-Tableau: 41 | - Tableau Server 42 | X-Ua-Compatible: 43 | - IE=Edge 44 | X-Xss-Protection: 45 | - 1; mode=block 46 | Transfer-Encoding: 47 | - chunked 48 | Connection: 49 | - keep-alive 50 | body: 51 | encoding: ASCII-8BIT 52 | string: 56 | http_version: 57 | recorded_at: Wed, 28 Oct 2020 20:40:16 GMT 58 | - request: 59 | method: get 60 | uri: http://TABLEAU_HOST/api/3.1/sites/f31923d2-dea4-4470-843a-9fc999d240aa/datasources?pageNumber=1&pageSize=100 61 | body: 62 | encoding: US-ASCII 63 | string: '' 64 | headers: 65 | X-Tableau-Auth: 66 | - V-Mj5mcBTJCdjUYTwGXbsQ|jvpee6clUK0B57aiJejDYRrMHhsm4s9h 67 | Accept-Encoding: 68 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 69 | Accept: 70 | - "*/*" 71 | User-Agent: 72 | - Ruby 73 | response: 74 | status: 75 | code: 200 76 | message: OK 77 | headers: 78 | Content-Security-Policy-Report-Only: 79 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 80 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 81 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 82 | Content-Type: 83 | - application/xml;charset=utf-8 84 | Date: 85 | - Wed, 28 Oct 2020 20:40:16 GMT 86 | P3p: 87 | - CP="NON" 88 | Server: 89 | - Tableau 90 | Vary: 91 | - Accept-Encoding 92 | X-Content-Type-Options: 93 | - nosniff 94 | X-Tableau: 95 | - Tableau Server 96 | X-Ua-Compatible: 97 | - IE=Edge 98 | X-Xss-Protection: 99 | - 1; mode=block 100 | Content-Length: 101 | - '469' 102 | Connection: 103 | - keep-alive 104 | body: 105 | encoding: ASCII-8BIT 106 | string: 114 | http_version: 115 | recorded_at: Wed, 28 Oct 2020 20:40:16 GMT 116 | recorded_with: VCR 3.0.3 117 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/groups.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: http://TABLEAU_HOST/api/3.1/auth/signin 6 | body: 7 | encoding: UTF-8 8 | string: 10 | headers: 11 | Content-Type: 12 | - application/xml 13 | Accept-Encoding: 14 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 15 | Accept: 16 | - "*/*" 17 | User-Agent: 18 | - Ruby 19 | response: 20 | status: 21 | code: 200 22 | message: OK 23 | headers: 24 | Content-Security-Policy-Report-Only: 25 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 26 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 27 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 28 | Content-Type: 29 | - application/xml;charset=utf-8 30 | Date: 31 | - Tue, 24 Nov 2020 22:20:57 GMT 32 | P3p: 33 | - CP="NON" 34 | Server: 35 | - Tableau 36 | Vary: 37 | - Accept-Encoding 38 | X-Content-Type-Options: 39 | - nosniff 40 | X-Tableau: 41 | - Tableau Server 42 | X-Ua-Compatible: 43 | - IE=Edge 44 | X-Xss-Protection: 45 | - 1; mode=block 46 | Transfer-Encoding: 47 | - chunked 48 | Connection: 49 | - keep-alive 50 | body: 51 | encoding: ASCII-8BIT 52 | string: 56 | http_version: 57 | recorded_at: Tue, 24 Nov 2020 22:20:57 GMT 58 | - request: 59 | method: post 60 | uri: http://TABLEAU_HOST/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/groups 61 | body: 62 | encoding: UTF-8 63 | string: 64 | headers: 65 | Content-Type: 66 | - application/xml 67 | X-Tableau-Auth: 68 | - 3BUXJOEXTiuty-6nSMXGzw|X12QskQTQyIU0hSubd6HRac46EKUtWTF 69 | Accept-Encoding: 70 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 71 | Accept: 72 | - "*/*" 73 | User-Agent: 74 | - Ruby 75 | response: 76 | status: 77 | code: 201 78 | message: Created 79 | headers: 80 | Content-Security-Policy-Report-Only: 81 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 82 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 83 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 84 | Content-Type: 85 | - application/xml;charset=utf-8 86 | Date: 87 | - Tue, 24 Nov 2020 22:20:57 GMT 88 | Location: 89 | - "/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/groups/90ff8ad8-bb15-404d-a84e-f3ffde19c902" 90 | P3p: 91 | - CP="NON" 92 | Server: 93 | - Tableau 94 | X-Content-Type-Options: 95 | - nosniff 96 | X-Tableau: 97 | - Tableau Server 98 | X-Ua-Compatible: 99 | - IE=Edge 100 | X-Xss-Protection: 101 | - 1; mode=block 102 | Transfer-Encoding: 103 | - chunked 104 | Connection: 105 | - keep-alive 106 | body: 107 | encoding: UTF-8 108 | string: 112 | http_version: 113 | recorded_at: Tue, 24 Nov 2020 22:20:57 GMT 114 | - request: 115 | method: get 116 | uri: http://TABLEAU_HOST/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/groups?pageNumber=1&pageSize=100 117 | body: 118 | encoding: US-ASCII 119 | string: '' 120 | headers: 121 | X-Tableau-Auth: 122 | - 3BUXJOEXTiuty-6nSMXGzw|X12QskQTQyIU0hSubd6HRac46EKUtWTF 123 | Accept-Encoding: 124 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 125 | Accept: 126 | - "*/*" 127 | User-Agent: 128 | - Ruby 129 | response: 130 | status: 131 | code: 200 132 | message: OK 133 | headers: 134 | Content-Security-Policy-Report-Only: 135 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 136 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 137 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 138 | Content-Type: 139 | - application/xml;charset=utf-8 140 | Date: 141 | - Tue, 24 Nov 2020 22:21:13 GMT 142 | P3p: 143 | - CP="NON" 144 | Server: 145 | - Tableau 146 | Vary: 147 | - Accept-Encoding 148 | X-Content-Type-Options: 149 | - nosniff 150 | X-Tableau: 151 | - Tableau Server 152 | X-Ua-Compatible: 153 | - IE=Edge 154 | X-Xss-Protection: 155 | - 1; mode=block 156 | Content-Length: 157 | - '320' 158 | Connection: 159 | - keep-alive 160 | body: 161 | encoding: ASCII-8BIT 162 | string: 168 | http_version: 169 | recorded_at: Tue, 24 Nov 2020 22:21:13 GMT 170 | - request: 171 | method: get 172 | uri: http://TABLEAU_HOST/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/users?pageNumber=1&pageSize=100 173 | body: 174 | encoding: US-ASCII 175 | string: '' 176 | headers: 177 | X-Tableau-Auth: 178 | - 3BUXJOEXTiuty-6nSMXGzw|X12QskQTQyIU0hSubd6HRac46EKUtWTF 179 | Accept-Encoding: 180 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 181 | Accept: 182 | - "*/*" 183 | User-Agent: 184 | - Ruby 185 | response: 186 | status: 187 | code: 200 188 | message: OK 189 | headers: 190 | Content-Security-Policy-Report-Only: 191 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 192 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 193 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 194 | Content-Type: 195 | - application/xml;charset=utf-8 196 | Date: 197 | - Tue, 24 Nov 2020 22:21:13 GMT 198 | P3p: 199 | - CP="NON" 200 | Server: 201 | - Tableau 202 | Vary: 203 | - Accept-Encoding 204 | X-Content-Type-Options: 205 | - nosniff 206 | X-Tableau: 207 | - Tableau Server 208 | X-Ua-Compatible: 209 | - IE=Edge 210 | X-Xss-Protection: 211 | - 1; mode=block 212 | Content-Length: 213 | - '420' 214 | Connection: 215 | - keep-alive 216 | body: 217 | encoding: ASCII-8BIT 218 | string: 226 | http_version: 227 | recorded_at: Tue, 24 Nov 2020 22:21:13 GMT 228 | - request: 229 | method: post 230 | uri: http://TABLEAU_HOST/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/groups/90ff8ad8-bb15-404d-a84e-f3ffde19c902/users 231 | body: 232 | encoding: UTF-8 233 | string: 234 | headers: 235 | Content-Type: 236 | - application/xml 237 | X-Tableau-Auth: 238 | - 3BUXJOEXTiuty-6nSMXGzw|X12QskQTQyIU0hSubd6HRac46EKUtWTF 239 | Accept-Encoding: 240 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 241 | Accept: 242 | - "*/*" 243 | User-Agent: 244 | - Ruby 245 | response: 246 | status: 247 | code: 200 248 | message: OK 249 | headers: 250 | Content-Security-Policy-Report-Only: 251 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 252 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 253 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 254 | Content-Type: 255 | - application/xml;charset=utf-8 256 | Date: 257 | - Tue, 24 Nov 2020 22:21:13 GMT 258 | P3p: 259 | - CP="NON" 260 | Server: 261 | - Tableau 262 | Vary: 263 | - Accept-Encoding 264 | X-Content-Type-Options: 265 | - nosniff 266 | X-Tableau: 267 | - Tableau Server 268 | X-Ua-Compatible: 269 | - IE=Edge 270 | X-Xss-Protection: 271 | - 1; mode=block 272 | Content-Length: 273 | - '251' 274 | Connection: 275 | - keep-alive 276 | body: 277 | encoding: ASCII-8BIT 278 | string: 282 | http_version: 283 | recorded_at: Tue, 24 Nov 2020 22:21:13 GMT 284 | - request: 285 | method: get 286 | uri: http://TABLEAU_HOST/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/groups/90ff8ad8-bb15-404d-a84e-f3ffde19c902/users?pageNumber=1&pageSize=100 287 | body: 288 | encoding: US-ASCII 289 | string: '' 290 | headers: 291 | X-Tableau-Auth: 292 | - 3BUXJOEXTiuty-6nSMXGzw|X12QskQTQyIU0hSubd6HRac46EKUtWTF 293 | Accept-Encoding: 294 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 295 | Accept: 296 | - "*/*" 297 | User-Agent: 298 | - Ruby 299 | response: 300 | status: 301 | code: 200 302 | message: OK 303 | headers: 304 | Content-Security-Policy-Report-Only: 305 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 306 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 307 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 308 | Content-Type: 309 | - application/xml;charset=utf-8 310 | Date: 311 | - Tue, 24 Nov 2020 22:21:13 GMT 312 | P3p: 313 | - CP="NON" 314 | Server: 315 | - Tableau 316 | Vary: 317 | - Accept-Encoding 318 | X-Content-Type-Options: 319 | - nosniff 320 | X-Tableau: 321 | - Tableau Server 322 | X-Ua-Compatible: 323 | - IE=Edge 324 | X-Xss-Protection: 325 | - 1; mode=block 326 | Content-Length: 327 | - '292' 328 | Connection: 329 | - keep-alive 330 | body: 331 | encoding: ASCII-8BIT 332 | string: 337 | http_version: 338 | recorded_at: Tue, 24 Nov 2020 22:21:13 GMT 339 | - request: 340 | method: delete 341 | uri: http://TABLEAU_HOST/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/groups/90ff8ad8-bb15-404d-a84e-f3ffde19c902/users/ce93aaeb-fb25-4fbd-a573-74b549f5e95b 342 | body: 343 | encoding: US-ASCII 344 | string: '' 345 | headers: 346 | X-Tableau-Auth: 347 | - 3BUXJOEXTiuty-6nSMXGzw|X12QskQTQyIU0hSubd6HRac46EKUtWTF 348 | Accept-Encoding: 349 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 350 | Accept: 351 | - "*/*" 352 | User-Agent: 353 | - Ruby 354 | response: 355 | status: 356 | code: 204 357 | message: No Content 358 | headers: 359 | Content-Security-Policy-Report-Only: 360 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 361 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 362 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 363 | Date: 364 | - Tue, 24 Nov 2020 22:21:14 GMT 365 | P3p: 366 | - CP="NON" 367 | Server: 368 | - Tableau 369 | X-Content-Type-Options: 370 | - nosniff 371 | X-Tableau: 372 | - Tableau Server 373 | X-Ua-Compatible: 374 | - IE=Edge 375 | X-Xss-Protection: 376 | - 1; mode=block 377 | Connection: 378 | - keep-alive 379 | body: 380 | encoding: UTF-8 381 | string: '' 382 | http_version: 383 | recorded_at: Tue, 24 Nov 2020 22:21:14 GMT 384 | recorded_with: VCR 3.0.3 385 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/jobs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: http://TABLEAU_HOST/api/3.1/auth/signin 6 | body: 7 | encoding: UTF-8 8 | string: 10 | headers: 11 | Accept-Encoding: 12 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 13 | Accept: 14 | - "*/*" 15 | User-Agent: 16 | - Ruby 17 | response: 18 | status: 19 | code: 200 20 | message: OK 21 | headers: 22 | Content-Security-Policy-Report-Only: 23 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 24 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 25 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 26 | Content-Type: 27 | - application/xml;charset=utf-8 28 | Date: 29 | - Mon, 30 Nov 2020 21:37:25 GMT 30 | P3p: 31 | - CP="NON" 32 | Server: 33 | - Tableau 34 | Vary: 35 | - Accept-Encoding 36 | X-Content-Type-Options: 37 | - nosniff 38 | X-Tableau: 39 | - Tableau Server 40 | X-Ua-Compatible: 41 | - IE=Edge 42 | X-Xss-Protection: 43 | - 1; mode=block 44 | Content-Length: 45 | - '329' 46 | Connection: 47 | - keep-alive 48 | body: 49 | encoding: ASCII-8BIT 50 | string: 54 | http_version: 55 | recorded_at: Mon, 30 Nov 2020 21:37:25 GMT 56 | - request: 57 | method: get 58 | uri: http://TABLEAU_HOST/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/projects?pageNumber=1&pageSize=100 59 | body: 60 | encoding: US-ASCII 61 | string: '' 62 | headers: 63 | User-Agent: 64 | - tableau_api/4.0.0 Ruby/2.4.1 65 | X-Tableau-Auth: 66 | - JLiIUYvpRIKOgQ5MtStpMQ|3P04IdDmWUI3nCSpmYIViDJvvTS6hVch 67 | Accept-Encoding: 68 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 69 | Accept: 70 | - "*/*" 71 | response: 72 | status: 73 | code: 200 74 | message: OK 75 | headers: 76 | Content-Security-Policy-Report-Only: 77 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 78 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 79 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 80 | Content-Type: 81 | - application/xml;charset=utf-8 82 | Date: 83 | - Mon, 30 Nov 2020 21:37:25 GMT 84 | P3p: 85 | - CP="NON" 86 | Server: 87 | - Tableau 88 | Vary: 89 | - Accept-Encoding 90 | X-Content-Type-Options: 91 | - nosniff 92 | X-Tableau: 93 | - Tableau Server 94 | X-Ua-Compatible: 95 | - IE=Edge 96 | X-Xss-Protection: 97 | - 1; mode=block 98 | Content-Length: 99 | - '522' 100 | Connection: 101 | - keep-alive 102 | body: 103 | encoding: ASCII-8BIT 104 | string: 116 | http_version: 117 | recorded_at: Mon, 30 Nov 2020 21:37:25 GMT 118 | - request: 119 | method: get 120 | uri: http://TABLEAU_HOST/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/users/962f69f4-db35-4a1d-b2b5-c5c5c5c5bcaa/workbooks?pageNumber=1&pageSize=100 121 | body: 122 | encoding: US-ASCII 123 | string: '' 124 | headers: 125 | User-Agent: 126 | - tableau_api/4.0.0 Ruby/2.4.1 127 | X-Tableau-Auth: 128 | - JLiIUYvpRIKOgQ5MtStpMQ|3P04IdDmWUI3nCSpmYIViDJvvTS6hVch 129 | Accept-Encoding: 130 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 131 | Accept: 132 | - "*/*" 133 | response: 134 | status: 135 | code: 200 136 | message: OK 137 | headers: 138 | Content-Security-Policy-Report-Only: 139 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 140 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 141 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 142 | Content-Type: 143 | - application/xml;charset=utf-8 144 | Date: 145 | - Mon, 30 Nov 2020 21:37:25 GMT 146 | P3p: 147 | - CP="NON" 148 | Server: 149 | - Tableau 150 | Vary: 151 | - Accept-Encoding 152 | X-Content-Type-Options: 153 | - nosniff 154 | X-Tableau: 155 | - Tableau Server 156 | X-Ua-Compatible: 157 | - IE=Edge 158 | X-Xss-Protection: 159 | - 1; mode=block 160 | Content-Length: 161 | - '659' 162 | Connection: 163 | - keep-alive 164 | body: 165 | encoding: ASCII-8BIT 166 | string: 188 | http_version: 189 | recorded_at: Mon, 30 Nov 2020 21:37:26 GMT 190 | - request: 191 | method: get 192 | uri: http://TABLEAU_HOST/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/workbooks/578ed308-3715-46ac-ae36-1af2caad6216 193 | body: 194 | encoding: US-ASCII 195 | string: '' 196 | headers: 197 | User-Agent: 198 | - tableau_api/4.0.0 Ruby/2.4.1 199 | X-Tableau-Auth: 200 | - JLiIUYvpRIKOgQ5MtStpMQ|3P04IdDmWUI3nCSpmYIViDJvvTS6hVch 201 | Accept-Encoding: 202 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 203 | Accept: 204 | - "*/*" 205 | response: 206 | status: 207 | code: 200 208 | message: OK 209 | headers: 210 | Content-Security-Policy-Report-Only: 211 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 212 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 213 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 214 | Content-Type: 215 | - application/xml;charset=utf-8 216 | Date: 217 | - Mon, 30 Nov 2020 21:37:25 GMT 218 | P3p: 219 | - CP="NON" 220 | Server: 221 | - Tableau 222 | Vary: 223 | - Accept-Encoding 224 | X-Content-Type-Options: 225 | - nosniff 226 | X-Tableau: 227 | - Tableau Server 228 | X-Ua-Compatible: 229 | - IE=Edge 230 | X-Xss-Protection: 231 | - 1; mode=block 232 | Content-Length: 233 | - '478' 234 | Connection: 235 | - keep-alive 236 | body: 237 | encoding: ASCII-8BIT 238 | string: 246 | http_version: 247 | recorded_at: Mon, 30 Nov 2020 21:37:26 GMT 248 | - request: 249 | method: post 250 | uri: http://TABLEAU_HOST/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/workbooks/578ed308-3715-46ac-ae36-1af2caad6216/refresh 251 | body: 252 | encoding: UTF-8 253 | string: "" 254 | headers: 255 | User-Agent: 256 | - tableau_api/4.0.0 Ruby/2.4.1 257 | Content-Type: 258 | - application/xml 259 | X-Tableau-Auth: 260 | - JLiIUYvpRIKOgQ5MtStpMQ|3P04IdDmWUI3nCSpmYIViDJvvTS6hVch 261 | Accept-Encoding: 262 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 263 | Accept: 264 | - "*/*" 265 | response: 266 | status: 267 | code: 202 268 | message: Accepted 269 | headers: 270 | Content-Security-Policy-Report-Only: 271 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 272 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 273 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 274 | Content-Type: 275 | - application/xml;charset=utf-8 276 | Date: 277 | - Mon, 30 Nov 2020 21:37:26 GMT 278 | Location: 279 | - "/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/jobs/4cd38c29-d21a-4efb-9ccc-b6055fd98dad" 280 | P3p: 281 | - CP="NON" 282 | Server: 283 | - Tableau 284 | X-Content-Type-Options: 285 | - nosniff 286 | X-Tableau: 287 | - Tableau Server 288 | X-Ua-Compatible: 289 | - IE=Edge 290 | X-Xss-Protection: 291 | - 1; mode=block 292 | Transfer-Encoding: 293 | - chunked 294 | Connection: 295 | - keep-alive 296 | body: 297 | encoding: UTF-8 298 | string: 303 | http_version: 304 | recorded_at: Mon, 30 Nov 2020 21:37:26 GMT 305 | - request: 306 | method: get 307 | uri: http://TABLEAU_HOST/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/jobs?pageNumber=1&pageSize=100 308 | body: 309 | encoding: US-ASCII 310 | string: '' 311 | headers: 312 | User-Agent: 313 | - tableau_api/4.0.0 Ruby/2.4.1 314 | X-Tableau-Auth: 315 | - JLiIUYvpRIKOgQ5MtStpMQ|3P04IdDmWUI3nCSpmYIViDJvvTS6hVch 316 | Accept-Encoding: 317 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 318 | Accept: 319 | - "*/*" 320 | response: 321 | status: 322 | code: 200 323 | message: OK 324 | headers: 325 | Content-Security-Policy-Report-Only: 326 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 327 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 328 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 329 | Content-Type: 330 | - application/xml;charset=utf-8 331 | Date: 332 | - Mon, 30 Nov 2020 21:37:26 GMT 333 | P3p: 334 | - CP="NON" 335 | Server: 336 | - Tableau 337 | Vary: 338 | - Accept-Encoding 339 | X-Content-Type-Options: 340 | - nosniff 341 | X-Tableau: 342 | - Tableau Server 343 | X-Ua-Compatible: 344 | - IE=Edge 345 | X-Xss-Protection: 346 | - 1; mode=block 347 | Transfer-Encoding: 348 | - chunked 349 | Connection: 350 | - keep-alive 351 | body: 352 | encoding: ASCII-8BIT 353 | string: 448 | http_version: 449 | recorded_at: Mon, 30 Nov 2020 21:37:27 GMT 450 | - request: 451 | method: get 452 | uri: http://TABLEAU_HOST/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/jobs?filter=jobType:eq:refresh_extracts,status:eq:Success&pageNumber=1&pageSize=100 453 | body: 454 | encoding: US-ASCII 455 | string: '' 456 | headers: 457 | User-Agent: 458 | - tableau_api/4.0.0 Ruby/2.4.1 459 | X-Tableau-Auth: 460 | - JLiIUYvpRIKOgQ5MtStpMQ|3P04IdDmWUI3nCSpmYIViDJvvTS6hVch 461 | Accept-Encoding: 462 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 463 | Accept: 464 | - "*/*" 465 | response: 466 | status: 467 | code: 200 468 | message: OK 469 | headers: 470 | Content-Security-Policy-Report-Only: 471 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 472 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 473 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 474 | Content-Type: 475 | - application/xml;charset=utf-8 476 | Date: 477 | - Mon, 30 Nov 2020 21:37:26 GMT 478 | P3p: 479 | - CP="NON" 480 | Server: 481 | - Tableau 482 | Vary: 483 | - Accept-Encoding 484 | X-Content-Type-Options: 485 | - nosniff 486 | X-Tableau: 487 | - Tableau Server 488 | X-Ua-Compatible: 489 | - IE=Edge 490 | X-Xss-Protection: 491 | - 1; mode=block 492 | Transfer-Encoding: 493 | - chunked 494 | Connection: 495 | - keep-alive 496 | body: 497 | encoding: ASCII-8BIT 498 | string: 502 | http_version: 503 | recorded_at: Mon, 30 Nov 2020 21:37:27 GMT 504 | recorded_with: VCR 3.0.3 505 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/projects.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: http://TABLEAU_HOST/api/3.1/auth/signin 6 | body: 7 | encoding: UTF-8 8 | string: 10 | headers: 11 | Content-Type: 12 | - application/xml 13 | Accept-Encoding: 14 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 15 | Accept: 16 | - "*/*" 17 | User-Agent: 18 | - Ruby 19 | response: 20 | status: 21 | code: 200 22 | message: OK 23 | headers: 24 | Content-Security-Policy-Report-Only: 25 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 26 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 27 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 28 | Content-Type: 29 | - application/xml;charset=utf-8 30 | Date: 31 | - Tue, 24 Nov 2020 21:59:47 GMT 32 | P3p: 33 | - CP="NON" 34 | Server: 35 | - Tableau 36 | Vary: 37 | - Accept-Encoding 38 | X-Content-Type-Options: 39 | - nosniff 40 | X-Tableau: 41 | - Tableau Server 42 | X-Ua-Compatible: 43 | - IE=Edge 44 | X-Xss-Protection: 45 | - 1; mode=block 46 | Content-Length: 47 | - '330' 48 | Connection: 49 | - keep-alive 50 | body: 51 | encoding: ASCII-8BIT 52 | string: 56 | http_version: 57 | recorded_at: Tue, 24 Nov 2020 21:59:47 GMT 58 | - request: 59 | method: post 60 | uri: http://TABLEAU_HOST/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/projects 61 | body: 62 | encoding: UTF-8 63 | string: 64 | headers: 65 | Content-Type: 66 | - application/xml 67 | X-Tableau-Auth: 68 | - E86lXTA6SW23T7zCtKhNQA|lh0ZYV3gilTVQkTeifS2g0BfPiPEtyJU 69 | Accept-Encoding: 70 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 71 | Accept: 72 | - "*/*" 73 | User-Agent: 74 | - Ruby 75 | response: 76 | status: 77 | code: 201 78 | message: Created 79 | headers: 80 | Content-Security-Policy-Report-Only: 81 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 82 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 83 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 84 | Content-Type: 85 | - application/xml;charset=utf-8 86 | Date: 87 | - Tue, 24 Nov 2020 21:59:47 GMT 88 | Location: 89 | - "/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/projects/965b0b2c-074f-48ff-8032-dbeaa5d22b27" 90 | P3p: 91 | - CP="NON" 92 | Server: 93 | - Tableau 94 | X-Content-Type-Options: 95 | - nosniff 96 | X-Tableau: 97 | - Tableau Server 98 | X-Ua-Compatible: 99 | - IE=Edge 100 | X-Xss-Protection: 101 | - 1; mode=block 102 | Content-Length: 103 | - '478' 104 | Connection: 105 | - keep-alive 106 | body: 107 | encoding: UTF-8 108 | string: 113 | http_version: 114 | recorded_at: Tue, 24 Nov 2020 21:59:47 GMT 115 | - request: 116 | method: get 117 | uri: http://TABLEAU_HOST/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/projects?pageNumber=1&pageSize=100 118 | body: 119 | encoding: US-ASCII 120 | string: '' 121 | headers: 122 | X-Tableau-Auth: 123 | - E86lXTA6SW23T7zCtKhNQA|lh0ZYV3gilTVQkTeifS2g0BfPiPEtyJU 124 | Accept-Encoding: 125 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 126 | Accept: 127 | - "*/*" 128 | User-Agent: 129 | - Ruby 130 | response: 131 | status: 132 | code: 200 133 | message: OK 134 | headers: 135 | Content-Security-Policy-Report-Only: 136 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 137 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 138 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 139 | Content-Type: 140 | - application/xml;charset=utf-8 141 | Date: 142 | - Tue, 24 Nov 2020 22:00:03 GMT 143 | P3p: 144 | - CP="NON" 145 | Server: 146 | - Tableau 147 | Vary: 148 | - Accept-Encoding 149 | X-Content-Type-Options: 150 | - nosniff 151 | X-Tableau: 152 | - Tableau Server 153 | X-Ua-Compatible: 154 | - IE=Edge 155 | X-Xss-Protection: 156 | - 1; mode=block 157 | Content-Length: 158 | - '522' 159 | Connection: 160 | - keep-alive 161 | body: 162 | encoding: ASCII-8BIT 163 | string: 175 | http_version: 176 | recorded_at: Tue, 24 Nov 2020 22:00:03 GMT 177 | recorded_with: VCR 3.0.3 178 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/sites.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: http://TABLEAU_HOST/api/3.1/auth/signin 6 | body: 7 | encoding: UTF-8 8 | string: 10 | headers: 11 | Content-Type: 12 | - application/xml 13 | Accept-Encoding: 14 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 15 | Accept: 16 | - "*/*" 17 | User-Agent: 18 | - Ruby 19 | response: 20 | status: 21 | code: 200 22 | message: OK 23 | headers: 24 | Content-Security-Policy-Report-Only: 25 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 26 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 27 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 28 | Content-Type: 29 | - application/xml;charset=utf-8 30 | Date: 31 | - Tue, 24 Nov 2020 22:00:03 GMT 32 | P3p: 33 | - CP="NON" 34 | Server: 35 | - Tableau 36 | Vary: 37 | - Accept-Encoding 38 | X-Content-Type-Options: 39 | - nosniff 40 | X-Tableau: 41 | - Tableau Server 42 | X-Ua-Compatible: 43 | - IE=Edge 44 | X-Xss-Protection: 45 | - 1; mode=block 46 | Content-Length: 47 | - '327' 48 | Connection: 49 | - keep-alive 50 | body: 51 | encoding: ASCII-8BIT 52 | string: 56 | http_version: 57 | recorded_at: Tue, 24 Nov 2020 22:00:03 GMT 58 | - request: 59 | method: post 60 | uri: http://TABLEAU_HOST/api/3.1/sites 61 | body: 62 | encoding: UTF-8 63 | string: 64 | headers: 65 | Content-Type: 66 | - application/xml 67 | X-Tableau-Auth: 68 | - qvSyAq_XTTiIrqf4GQBUiw|xQZk64kPTOCyxdzlPYwe1P3pg4C58z3H 69 | Accept-Encoding: 70 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 71 | Accept: 72 | - "*/*" 73 | User-Agent: 74 | - Ruby 75 | response: 76 | status: 77 | code: 201 78 | message: Created 79 | headers: 80 | Content-Security-Policy-Report-Only: 81 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 82 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 83 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 84 | Content-Type: 85 | - application/xml;charset=utf-8 86 | Date: 87 | - Tue, 24 Nov 2020 22:00:03 GMT 88 | Location: 89 | - "/api/3.1/sites/33548c67-b955-49f0-8e0a-8d6f6ee207e9" 90 | P3p: 91 | - CP="NON" 92 | Server: 93 | - Tableau 94 | X-Content-Type-Options: 95 | - nosniff 96 | X-Tableau: 97 | - Tableau Server 98 | X-Ua-Compatible: 99 | - IE=Edge 100 | X-Xss-Protection: 101 | - 1; mode=block 102 | Content-Length: 103 | - '549' 104 | Connection: 105 | - keep-alive 106 | body: 107 | encoding: UTF-8 108 | string: 114 | http_version: 115 | recorded_at: Tue, 24 Nov 2020 22:00:03 GMT 116 | - request: 117 | method: post 118 | uri: http://TABLEAU_HOST/api/3.1/sites 119 | body: 120 | encoding: UTF-8 121 | string: 122 | headers: 123 | Content-Type: 124 | - application/xml 125 | X-Tableau-Auth: 126 | - qvSyAq_XTTiIrqf4GQBUiw|xQZk64kPTOCyxdzlPYwe1P3pg4C58z3H 127 | Accept-Encoding: 128 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 129 | Accept: 130 | - "*/*" 131 | User-Agent: 132 | - Ruby 133 | response: 134 | status: 135 | code: 409 136 | message: Conflict 137 | headers: 138 | Content-Security-Policy-Report-Only: 139 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 140 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 141 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 142 | Content-Type: 143 | - application/xml;charset=utf-8 144 | Date: 145 | - Tue, 24 Nov 2020 22:00:04 GMT 146 | P3p: 147 | - CP="NON" 148 | Server: 149 | - Tableau 150 | X-Content-Type-Options: 151 | - nosniff 152 | X-Tableau: 153 | - Tableau Server 154 | X-Ua-Compatible: 155 | - IE=Edge 156 | X-Xss-Protection: 157 | - 1; mode=block 158 | Content-Length: 159 | - '366' 160 | Connection: 161 | - keep-alive 162 | body: 163 | encoding: UTF-8 164 | string: Resource 167 | ConflictA site already exists with the content URL 'TestSite2' 168 | http_version: 169 | recorded_at: Tue, 24 Nov 2020 22:00:04 GMT 170 | - request: 171 | method: post 172 | uri: http://TABLEAU_HOST/api/3.1/auth/signin 173 | body: 174 | encoding: UTF-8 175 | string: 177 | headers: 178 | Content-Type: 179 | - application/xml 180 | Accept-Encoding: 181 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 182 | Accept: 183 | - "*/*" 184 | User-Agent: 185 | - Ruby 186 | response: 187 | status: 188 | code: 200 189 | message: OK 190 | headers: 191 | Content-Security-Policy-Report-Only: 192 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 193 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 194 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 195 | Content-Type: 196 | - application/xml;charset=utf-8 197 | Date: 198 | - Tue, 24 Nov 2020 22:00:04 GMT 199 | P3p: 200 | - CP="NON" 201 | Server: 202 | - Tableau 203 | Vary: 204 | - Accept-Encoding 205 | X-Content-Type-Options: 206 | - nosniff 207 | X-Tableau: 208 | - Tableau Server 209 | X-Ua-Compatible: 210 | - IE=Edge 211 | X-Xss-Protection: 212 | - 1; mode=block 213 | Content-Length: 214 | - '330' 215 | Connection: 216 | - keep-alive 217 | body: 218 | encoding: ASCII-8BIT 219 | string: 223 | http_version: 224 | recorded_at: Tue, 24 Nov 2020 22:00:04 GMT 225 | - request: 226 | method: get 227 | uri: http://TABLEAU_HOST/api/3.1/sites?pageNumber=1&pageSize=100 228 | body: 229 | encoding: US-ASCII 230 | string: '' 231 | headers: 232 | X-Tableau-Auth: 233 | - CGQsDHeZRG6Ntay3oguN8g|LokUHBbcO1MXyACKeZQ1mSETDz2HHlKR 234 | Accept-Encoding: 235 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 236 | Accept: 237 | - "*/*" 238 | User-Agent: 239 | - Ruby 240 | response: 241 | status: 242 | code: 200 243 | message: OK 244 | headers: 245 | Content-Security-Policy-Report-Only: 246 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 247 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 248 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 249 | Content-Type: 250 | - application/xml;charset=utf-8 251 | Date: 252 | - Tue, 24 Nov 2020 22:00:04 GMT 253 | P3p: 254 | - CP="NON" 255 | Server: 256 | - Tableau 257 | Vary: 258 | - Accept-Encoding 259 | X-Content-Type-Options: 260 | - nosniff 261 | X-Tableau: 262 | - Tableau Server 263 | X-Ua-Compatible: 264 | - IE=Edge 265 | X-Xss-Protection: 266 | - 1; mode=block 267 | Content-Length: 268 | - '3805' 269 | Connection: 270 | - keep-alive 271 | body: 272 | encoding: ASCII-8BIT 273 | string: 287 | http_version: 288 | recorded_at: Tue, 24 Nov 2020 22:00:04 GMT 289 | - request: 290 | method: post 291 | uri: http://TABLEAU_HOST/api/3.1/auth/signin 292 | body: 293 | encoding: UTF-8 294 | string: 296 | headers: 297 | Content-Type: 298 | - application/xml 299 | Accept-Encoding: 300 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 301 | Accept: 302 | - "*/*" 303 | User-Agent: 304 | - Ruby 305 | response: 306 | status: 307 | code: 200 308 | message: OK 309 | headers: 310 | Content-Security-Policy-Report-Only: 311 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 312 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 313 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 314 | Content-Type: 315 | - application/xml;charset=utf-8 316 | Date: 317 | - Tue, 24 Nov 2020 22:00:05 GMT 318 | P3p: 319 | - CP="NON" 320 | Server: 321 | - Tableau 322 | Vary: 323 | - Accept-Encoding 324 | X-Content-Type-Options: 325 | - nosniff 326 | X-Tableau: 327 | - Tableau Server 328 | X-Ua-Compatible: 329 | - IE=Edge 330 | X-Xss-Protection: 331 | - 1; mode=block 332 | Content-Length: 333 | - '330' 334 | Connection: 335 | - keep-alive 336 | body: 337 | encoding: ASCII-8BIT 338 | string: 342 | http_version: 343 | recorded_at: Tue, 24 Nov 2020 22:00:05 GMT 344 | - request: 345 | method: delete 346 | uri: http://TABLEAU_HOST/api/3.1/sites/33548c67-b955-49f0-8e0a-8d6f6ee207e9 347 | body: 348 | encoding: US-ASCII 349 | string: '' 350 | headers: 351 | X-Tableau-Auth: 352 | - S0H_LNZ4SIGtHH8BJ29oUw|saC6UTopyudpr6p6TZ6ycFSnezGWeTvr 353 | Accept-Encoding: 354 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 355 | Accept: 356 | - "*/*" 357 | User-Agent: 358 | - Ruby 359 | response: 360 | status: 361 | code: 204 362 | message: No Content 363 | headers: 364 | Content-Security-Policy-Report-Only: 365 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 366 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 367 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 368 | Date: 369 | - Tue, 24 Nov 2020 22:00:05 GMT 370 | P3p: 371 | - CP="NON" 372 | Server: 373 | - Tableau 374 | X-Content-Type-Options: 375 | - nosniff 376 | X-Tableau: 377 | - Tableau Server 378 | X-Ua-Compatible: 379 | - IE=Edge 380 | X-Xss-Protection: 381 | - 1; mode=block 382 | Connection: 383 | - keep-alive 384 | body: 385 | encoding: UTF-8 386 | string: '' 387 | http_version: 388 | recorded_at: Tue, 24 Nov 2020 22:00:05 GMT 389 | - request: 390 | method: delete 391 | uri: http://TABLEAU_HOST/api/3.1/sites/does-not-exist 392 | body: 393 | encoding: US-ASCII 394 | string: '' 395 | headers: 396 | X-Tableau-Auth: 397 | - qvSyAq_XTTiIrqf4GQBUiw|xQZk64kPTOCyxdzlPYwe1P3pg4C58z3H 398 | Accept-Encoding: 399 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 400 | Accept: 401 | - "*/*" 402 | User-Agent: 403 | - Ruby 404 | response: 405 | status: 406 | code: 404 407 | message: Not Found 408 | headers: 409 | Content-Security-Policy-Report-Only: 410 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 411 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 412 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 413 | Content-Type: 414 | - application/xml;charset=utf-8 415 | Date: 416 | - Tue, 24 Nov 2020 22:00:05 GMT 417 | P3p: 418 | - CP="NON" 419 | Server: 420 | - Tableau 421 | X-Content-Type-Options: 422 | - nosniff 423 | X-Tableau: 424 | - Tableau Server 425 | X-Ua-Compatible: 426 | - IE=Edge 427 | X-Xss-Protection: 428 | - 1; mode=block 429 | Content-Length: 430 | - '354' 431 | Connection: 432 | - keep-alive 433 | body: 434 | encoding: UTF-8 435 | string: Resource 438 | Not FoundSite 'does-not-exist' could not be found. 439 | http_version: 440 | recorded_at: Tue, 24 Nov 2020 22:00:06 GMT 441 | recorded_with: VCR 3.0.3 442 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/users.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: http://TABLEAU_HOST/api/3.1/auth/signin 6 | body: 7 | encoding: UTF-8 8 | string: 10 | headers: 11 | Content-Type: 12 | - application/xml 13 | Accept-Encoding: 14 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 15 | Accept: 16 | - "*/*" 17 | User-Agent: 18 | - Ruby 19 | response: 20 | status: 21 | code: 200 22 | message: OK 23 | headers: 24 | Content-Security-Policy-Report-Only: 25 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 26 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 27 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 28 | Content-Type: 29 | - application/xml;charset=utf-8 30 | Date: 31 | - Tue, 24 Nov 2020 22:00:06 GMT 32 | P3p: 33 | - CP="NON" 34 | Server: 35 | - Tableau 36 | Vary: 37 | - Accept-Encoding 38 | X-Content-Type-Options: 39 | - nosniff 40 | X-Tableau: 41 | - Tableau Server 42 | X-Ua-Compatible: 43 | - IE=Edge 44 | X-Xss-Protection: 45 | - 1; mode=block 46 | Content-Length: 47 | - '331' 48 | Connection: 49 | - keep-alive 50 | body: 51 | encoding: ASCII-8BIT 52 | string: 56 | http_version: 57 | recorded_at: Tue, 24 Nov 2020 22:00:06 GMT 58 | - request: 59 | method: post 60 | uri: http://TABLEAU_HOST/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/users 61 | body: 62 | encoding: UTF-8 63 | string: 64 | headers: 65 | Content-Type: 66 | - application/xml 67 | X-Tableau-Auth: 68 | - DaJ94DvRTXG9DmnQtMxo_Q|dewvIZbNaq6Xg8pSWZndgbySnBJcV8jv 69 | Accept-Encoding: 70 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 71 | Accept: 72 | - "*/*" 73 | User-Agent: 74 | - Ruby 75 | response: 76 | status: 77 | code: 201 78 | message: Created 79 | headers: 80 | Content-Security-Policy-Report-Only: 81 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 82 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 83 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 84 | Content-Type: 85 | - application/xml;charset=utf-8 86 | Date: 87 | - Tue, 24 Nov 2020 22:00:06 GMT 88 | Location: 89 | - "/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/users/ce93aaeb-fb25-4fbd-a573-74b549f5e95b" 90 | P3p: 91 | - CP="NON" 92 | Server: 93 | - Tableau 94 | X-Content-Type-Options: 95 | - nosniff 96 | X-Tableau: 97 | - Tableau Server 98 | X-Ua-Compatible: 99 | - IE=Edge 100 | X-Xss-Protection: 101 | - 1; mode=block 102 | Content-Length: 103 | - '371' 104 | Connection: 105 | - keep-alive 106 | body: 107 | encoding: UTF-8 108 | string: 112 | http_version: 113 | recorded_at: Tue, 24 Nov 2020 22:00:06 GMT 114 | - request: 115 | method: get 116 | uri: http://TABLEAU_HOST/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/users?pageNumber=1&pageSize=100 117 | body: 118 | encoding: US-ASCII 119 | string: '' 120 | headers: 121 | X-Tableau-Auth: 122 | - DaJ94DvRTXG9DmnQtMxo_Q|dewvIZbNaq6Xg8pSWZndgbySnBJcV8jv 123 | Accept-Encoding: 124 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 125 | Accept: 126 | - "*/*" 127 | User-Agent: 128 | - Ruby 129 | response: 130 | status: 131 | code: 200 132 | message: OK 133 | headers: 134 | Content-Security-Policy-Report-Only: 135 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 136 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 137 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 138 | Content-Type: 139 | - application/xml;charset=utf-8 140 | Date: 141 | - Tue, 24 Nov 2020 22:00:22 GMT 142 | P3p: 143 | - CP="NON" 144 | Server: 145 | - Tableau 146 | Vary: 147 | - Accept-Encoding 148 | X-Content-Type-Options: 149 | - nosniff 150 | X-Tableau: 151 | - Tableau Server 152 | X-Ua-Compatible: 153 | - IE=Edge 154 | X-Xss-Protection: 155 | - 1; mode=block 156 | Content-Length: 157 | - '419' 158 | Connection: 159 | - keep-alive 160 | body: 161 | encoding: ASCII-8BIT 162 | string: 170 | http_version: 171 | recorded_at: Tue, 24 Nov 2020 22:00:22 GMT 172 | - request: 173 | method: get 174 | uri: http://TABLEAU_HOST/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/users/ce93aaeb-fb25-4fbd-a573-74b549f5e95b 175 | body: 176 | encoding: US-ASCII 177 | string: '' 178 | headers: 179 | X-Tableau-Auth: 180 | - DaJ94DvRTXG9DmnQtMxo_Q|dewvIZbNaq6Xg8pSWZndgbySnBJcV8jv 181 | Accept-Encoding: 182 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 183 | Accept: 184 | - "*/*" 185 | User-Agent: 186 | - Ruby 187 | response: 188 | status: 189 | code: 200 190 | message: OK 191 | headers: 192 | Content-Security-Policy-Report-Only: 193 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 194 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 195 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 196 | Content-Type: 197 | - application/xml;charset=utf-8 198 | Date: 199 | - Tue, 24 Nov 2020 22:00:22 GMT 200 | P3p: 201 | - CP="NON" 202 | Server: 203 | - Tableau 204 | Vary: 205 | - Accept-Encoding 206 | X-Content-Type-Options: 207 | - nosniff 208 | X-Tableau: 209 | - Tableau Server 210 | X-Ua-Compatible: 211 | - IE=Edge 212 | X-Xss-Protection: 213 | - 1; mode=block 214 | Content-Length: 215 | - '299' 216 | Connection: 217 | - keep-alive 218 | body: 219 | encoding: ASCII-8BIT 220 | string: 225 | http_version: 226 | recorded_at: Tue, 24 Nov 2020 22:00:22 GMT 227 | - request: 228 | method: put 229 | uri: http://TABLEAU_HOST/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/users/ce93aaeb-fb25-4fbd-a573-74b549f5e95b 230 | body: 231 | encoding: UTF-8 232 | string: 233 | headers: 234 | Content-Type: 235 | - application/xml 236 | X-Tableau-Auth: 237 | - DaJ94DvRTXG9DmnQtMxo_Q|dewvIZbNaq6Xg8pSWZndgbySnBJcV8jv 238 | Accept-Encoding: 239 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 240 | Accept: 241 | - "*/*" 242 | User-Agent: 243 | - Ruby 244 | response: 245 | status: 246 | code: 200 247 | message: OK 248 | headers: 249 | Content-Security-Policy-Report-Only: 250 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 251 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 252 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 253 | Content-Type: 254 | - application/xml;charset=utf-8 255 | Date: 256 | - Tue, 24 Nov 2020 22:00:22 GMT 257 | P3p: 258 | - CP="NON" 259 | Server: 260 | - Tableau 261 | Vary: 262 | - Accept-Encoding 263 | X-Content-Type-Options: 264 | - nosniff 265 | X-Tableau: 266 | - Tableau Server 267 | X-Ua-Compatible: 268 | - IE=Edge 269 | X-Xss-Protection: 270 | - 1; mode=block 271 | Content-Length: 272 | - '207' 273 | Connection: 274 | - keep-alive 275 | body: 276 | encoding: ASCII-8BIT 277 | string: 280 | http_version: 281 | recorded_at: Tue, 24 Nov 2020 22:00:22 GMT 282 | - request: 283 | method: get 284 | uri: http://TABLEAU_HOST/api/3.1/sites/fb50c166-f809-44e0-995e-2cf56ceffbc0/users/foo 285 | body: 286 | encoding: US-ASCII 287 | string: '' 288 | headers: 289 | X-Tableau-Auth: 290 | - DaJ94DvRTXG9DmnQtMxo_Q|dewvIZbNaq6Xg8pSWZndgbySnBJcV8jv 291 | Accept-Encoding: 292 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 293 | Accept: 294 | - "*/*" 295 | User-Agent: 296 | - Ruby 297 | response: 298 | status: 299 | code: 404 300 | message: Not Found 301 | headers: 302 | Content-Security-Policy-Report-Only: 303 | - 'connect-src * https://*.tiles.mapbox.com https://api.mapbox.com; default-src 304 | blob:; font-src * data:; frame-src * data:; img-src * data: blob:; object-src 305 | data:; report-uri /vizql/csp-report; script-src * blob:; style-src * ''unsafe-inline''' 306 | Content-Type: 307 | - application/xml;charset=utf-8 308 | Date: 309 | - Tue, 24 Nov 2020 22:00:22 GMT 310 | P3p: 311 | - CP="NON" 312 | Server: 313 | - Tableau 314 | X-Content-Type-Options: 315 | - nosniff 316 | X-Tableau: 317 | - Tableau Server 318 | X-Ua-Compatible: 319 | - IE=Edge 320 | X-Xss-Protection: 321 | - 1; mode=block 322 | Content-Length: 323 | - '343' 324 | Connection: 325 | - keep-alive 326 | body: 327 | encoding: UTF-8 328 | string: Resource 331 | Not FoundUser 'foo' could not be found. 332 | http_version: 333 | recorded_at: Tue, 24 Nov 2020 22:00:23 GMT 334 | recorded_with: VCR 3.0.3 335 | -------------------------------------------------------------------------------- /spec/fixtures/workbooks/test.twbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/civisanalytics/tableau_api/2e4b44381c3cb295ac933948733be3b8d7f6f02f/spec/fixtures/workbooks/test.twbx -------------------------------------------------------------------------------- /spec/resources/auth_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe TableauApi::Resources::Auth, vcr: { cassette_name: 'auth' } do 6 | let(:client) do 7 | TableauApi.new( 8 | host: ENV.fetch('TABLEAU_HOST', nil), 9 | site_name: 'Default', 10 | username: ENV.fetch('TABLEAU_ADMIN_USERNAME', nil), 11 | password: ENV.fetch('TABLEAU_ADMIN_PASSWORD', nil) 12 | ) 13 | end 14 | 15 | # http://onlinehelp.tableau.com/v9.0/api/rest_api/en-us/help.htm#REST/rest_api_concepts_auth.htm%3FTocPath%3DConcepts%7C_____3 16 | # http://onlinehelp.tableau.com/v9.0/api/rest_api/en-us/help.htm#REST/rest_api_ref.htm#Sign_In%3FTocPath%3DAPI%2520Reference%7C_____51 17 | it 'returns an instance of TableauApi::Resources::Auth' do 18 | client = TableauApi.new(host: 'tableau.domain.tld', site_name: 'Default', username: 'ExampleUsername', password: 'ExamplePassword') 19 | expect(client.auth).to be_an_instance_of(TableauApi::Resources::Auth) 20 | end 21 | 22 | it 'fails appropriately with a bad username or password' do 23 | client = TableauApi.new(host: ENV.fetch('TABLEAU_HOST', nil), site_name: 'Default', username: 'foo', password: 'bar') 24 | expect(client.auth.sign_in).to be false 25 | end 26 | 27 | it 'automatically signs in to get the token' do 28 | expect(client.auth.token).to be_a_token 29 | end 30 | 31 | it 'sucessfully signs in with a correct username and password' do 32 | expect(client.auth.sign_in).to be true 33 | expect(client.auth.token).to be_a_token 34 | expect(client.auth.site_id).to be_a_tableau_id 35 | expect(client.auth.user_id).to be_a_tableau_id 36 | end 37 | 38 | it 'signs into a different site' do 39 | client = TableauApi.new( 40 | host: ENV.fetch('TABLEAU_HOST', nil), 41 | site_name: 'TestSite', 42 | username: ENV.fetch('TABLEAU_ADMIN_USERNAME', nil), 43 | password: ENV.fetch('TABLEAU_ADMIN_PASSWORD', nil) 44 | ) 45 | expect(client.auth.sign_in).to be true 46 | expect(client.auth.token).to be_a_token 47 | expect(client.auth.site_id).to be_a_tableau_id 48 | expect(client.auth.user_id).to be_a_tableau_id 49 | end 50 | 51 | it 'does not sign in if already signed in' do 52 | expect(client.auth.sign_in).to be true 53 | token = client.auth.token 54 | 55 | expect(client.auth.sign_in).to be true 56 | expect(client.auth.token).to eq token 57 | end 58 | 59 | describe '.sign_out' do 60 | # http://onlinehelp.tableau.com/v9.0/api/rest_api/en-us/help.htm#REST/rest_api_ref.htm#Sign_Out 61 | it 'can sign out' do 62 | expect(client.auth.sign_in).to be true 63 | expect(client.auth.token).to be_a_token 64 | expect(client.auth.site_id).to be_a_tableau_id 65 | expect(client.auth.user_id).to be_a_tableau_id 66 | expect(client.auth.sign_out).to be true 67 | # accessing the token automatically signs in 68 | expect(client.auth.instance_variable_get('@token')).to be nil 69 | expect(client.auth.instance_variable_get('@site_id')).to be nil 70 | expect(client.auth.instance_variable_get('@user_id')).to be nil 71 | end 72 | 73 | it 'can sign out with a bad token' do 74 | expect(client.auth.sign_in).to be true 75 | client.auth.instance_variable_set('@token', 'foo') 76 | expect(client.auth.sign_out).to be true 77 | expect(client.auth.instance_variable_get('@token')).to be nil 78 | expect(client.auth.instance_variable_get('@site_id')).to be nil 79 | expect(client.auth.instance_variable_get('@user_id')).to be nil 80 | end 81 | end 82 | 83 | describe '.trusted_ticket' do 84 | it 'can get a trusted ticket' do 85 | client = TableauApi.new(host: ENV.fetch('TABLEAU_HOST', nil), site_name: 'Default', username: 'test') 86 | expect(client.auth.trusted_ticket).to be_a_trusted_ticket 87 | end 88 | 89 | it 'can get a trusted ticket for a non default site' do 90 | client = TableauApi.new(host: ENV.fetch('TABLEAU_HOST', nil), site_name: 'test', username: 'test_test') 91 | expect(client.auth.trusted_ticket).to be_a_trusted_ticket 92 | end 93 | 94 | it 'fails with a user not in a site' do 95 | client = TableauApi.new(host: ENV.fetch('TABLEAU_HOST', nil), site_name: 'test', username: 'test') 96 | expect(client.auth.trusted_ticket).to be nil 97 | end 98 | 99 | it 'fails with a bad user' do 100 | client = TableauApi.new(host: ENV.fetch('TABLEAU_HOST', nil), site_name: 'Default', username: 'invalid_user') 101 | expect(client.auth.trusted_ticket).to be nil 102 | end 103 | 104 | it 'fails with a bad site' do 105 | client = TableauApi.new(host: ENV.fetch('TABLEAU_HOST', nil), site_name: 'invalid_site', username: 'test') 106 | expect(client.auth.trusted_ticket).to be nil 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /spec/resources/datasources_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | describe TableauApi::Resources::Datasources, vcr: { cassette_name: 'datasources' } do 5 | include_context 'tableau client' 6 | 7 | # https://onlinehelp.tableau.com/current/api/rest_api/en-us/help.htm#REST/rest_api_ref.htm#Query_Datasources%3FTocPath%3DAPI%2520Reference%7C_____48 8 | 9 | it 'can list datasources' do 10 | datasource = client.datasources.list.find do |d| 11 | d['name'] == 'test' 12 | end 13 | expect(datasource['id']).to be_a_tableau_id 14 | expect(datasource).to eq( 15 | 'id' => datasource['id'], 16 | 'name' => 'test', 17 | 'contentUrl' => 'test', 18 | 'createdAt' => datasource['createdAt'], 19 | 'updatedAt' => datasource['updatedAt'], 20 | 'owner' => { 'id' => datasource['owner']['id'] }, 21 | 'project' => { 22 | 'id' => datasource['project']['id'], 23 | 'name' => datasource['project']['name'] 24 | }, 25 | 'type' => 'textscan', 26 | 'tags' => nil, 27 | 'isCertified' => 'false', 28 | 'webpageUrl' => datasource['webpageUrl'] 29 | ) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/resources/groups_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe TableauApi::Resources::Groups, vcr: { cassette_name: 'groups' } do 6 | include_context 'tableau client' 7 | 8 | let(:test_user_id) do 9 | user = client.users.list.find do |u| 10 | u['name'] == 'test' 11 | end 12 | user['id'] 13 | end 14 | 15 | describe '#create' do 16 | # http://onlinehelp.tableau.com/v9.0/api/rest_api/en-us/help.htm#REST/rest_api_ref.htm#Create_Group 17 | it 'fails with a bad site_role' do 18 | expect { client.groups.create(name: 'test', default_site_role: 'foo') }.to raise_error('invalid default_site_role') 19 | end 20 | 21 | it 'can create a group in a site' do 22 | group = client.groups.create(name: 'testgroup') 23 | expect(group['id']).to be_a_tableau_id 24 | expect(group).to eq('id' => group['id'], 'name' => 'testgroup') 25 | end 26 | end 27 | 28 | describe '#list' do 29 | # http://onlinehelp.tableau.com/v9.0/api/rest_api/en-us/help.htm#REST/rest_api_ref.htm#Query_Groups 30 | it 'can list groups in a site' do 31 | sleep(15) if VCR.current_cassette.recording? 32 | group = find_group_by_name('testgroup') 33 | expect(group).to eq('id' => group['id'], 'name' => 'testgroup', 'domain' => { 'name' => 'local' }) 34 | expect(group['id']).to be_a_tableau_id 35 | end 36 | end 37 | 38 | describe '#add_user' do 39 | it 'can add a user to a group' do 40 | group = find_group_by_name('testgroup') 41 | expect(client.groups.add_user(group_id: group['id'], user_id: test_user_id)).to be true 42 | end 43 | end 44 | 45 | describe '#users' do 46 | it 'can get the users in a group' do 47 | group = find_group_by_name('testgroup') 48 | users = client.groups.users(group_id: group['id']) 49 | expect(users.map { |u| u['id'] }.include?(test_user_id)).to be true 50 | end 51 | end 52 | 53 | describe '#remove_user' do 54 | it 'can remove a user from a group' do 55 | group = find_group_by_name('testgroup') 56 | expect(client.groups.remove_user(group_id: group['id'], user_id: test_user_id)).to be true 57 | end 58 | end 59 | 60 | def find_group_by_name(name) 61 | client.groups.list.find do |g| 62 | g['name'] == name 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/resources/jobs_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe TableauApi::Resources::Jobs, vcr: { cassette_name: 'jobs' } do 6 | include_context 'tableau client' 7 | 8 | def find_or_publish_workbook(name) 9 | project = client.projects.list.find { |p| p['name'] == 'test' } 10 | workbook = client.workbooks.list.find { |w| w['name'] == name } 11 | 12 | if workbook 13 | workbook = client.workbooks.find(workbook['id']) 14 | return workbook 15 | end 16 | 17 | client.workbooks.publish( 18 | name: name, 19 | project_id: project['id'], 20 | file: 'spec/fixtures/workbooks/test.twbx', 21 | overwrite: true 22 | ) 23 | end 24 | 25 | describe '#list' do 26 | # https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_jobstasksschedules.htm#query_jobs 27 | it 'can list jobs in a site' do 28 | workbook = find_or_publish_workbook('test') 29 | client.workbooks.refresh(workbook_id: workbook['id']) 30 | jobs = client.jobs.list 31 | expect(jobs.to_a.last.keys).to include('createdAt', 'id', 'jobType', 'priority', 'status') 32 | end 33 | 34 | it 'can filter jobs in a site' do 35 | workbook = find_or_publish_workbook('test') 36 | client.workbooks.refresh(workbook_id: workbook['id']) 37 | jobs = client.jobs.list(query: 'filter=jobType:eq:refresh_extracts,status:eq:Success') 38 | expect(jobs.to_a).to be_empty 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/resources/projects_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe TableauApi::Resources::Projects, vcr: { cassette_name: 'projects' } do 6 | include_context 'tableau client' 7 | 8 | # http://onlinehelp.tableau.com/v9.0/api/rest_api/en-us/help.htm#REST/rest_api_ref.htm#Create_Project%3FTocPath%3DAPI%2520Reference%7C_____13 9 | it 'can create a project in a site' do 10 | project = client.projects.create(name: 'test_project') 11 | expect(project['id']).to be_a_tableau_id 12 | expect(project).to eq( 13 | 'id' => project['id'], 14 | 'name' => 'test_project', 15 | 'description' => '', 16 | 'contentPermissions' => 'ManagedByOwner', 17 | 'owner' => project['owner'], 18 | 'updatedAt' => project['updatedAt'], 19 | 'createdAt' => project['createdAt'] 20 | ) 21 | end 22 | 23 | it 'can list projects' do 24 | sleep(15) if VCR.current_cassette.recording? 25 | project = client.projects.list.find do |p| 26 | p['name'] == 'test_project' 27 | end 28 | expect(project['id']).to be_a_tableau_id 29 | expect(project).to eq( 30 | 'id' => project['id'], 31 | 'name' => 'test_project', 32 | 'description' => '', 33 | 'contentPermissions' => 'ManagedByOwner', 34 | 'owner' => project['owner'], 35 | 'updatedAt' => project['updatedAt'], 36 | 'createdAt' => project['createdAt'] 37 | ) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/resources/sites_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe TableauApi::Resources::Sites, vcr: { cassette_name: 'sites' } do 6 | include_context 'tableau client' 7 | 8 | let(:default_client) do 9 | TableauApi.new( 10 | host: ENV.fetch('TABLEAU_HOST', nil), 11 | site_name: 'Default', 12 | username: ENV.fetch('TABLEAU_ADMIN_USERNAME', nil), 13 | password: ENV.fetch('TABLEAU_ADMIN_PASSWORD', nil) 14 | ) 15 | end 16 | 17 | describe '#create' do 18 | it 'can create a site' do 19 | site = default_client.sites.create(name: 'Test Site 2', content_url: 'TestSite2', admin_mode: 'ContentAndUsers') 20 | expect(site['id']).to be_a_tableau_id 21 | expect(site).to eq( 22 | 'id' => site['id'], 23 | 'name' => 'Test Site 2', 24 | 'contentUrl' => 'TestSite2', 25 | 'adminMode' => 'ContentAndUsers', 26 | 'state' => 'Active', 27 | 'disableSubscriptions' => 'false', 28 | 'cacheWarmupEnabled' => 'true', 29 | 'commentingEnabled' => 'true', 30 | 'guestAccessEnabled' => 'true', 31 | 'revisionHistoryEnabled' => 'true', 32 | 'revisionLimit' => '25', 33 | 'subscribeOthersEnabled' => 'true' 34 | ) 35 | end 36 | 37 | it 'raises an error if fails to create a site' do 38 | expect do 39 | default_client.sites.create(name: 'Test Site 3', content_url: 'TestSite2', admin_mode: 'ContentAndUsers') 40 | end.to raise_error(TableauApi::TableauError) 41 | end 42 | end 43 | 44 | describe '#list' do 45 | # http://onlinehelp.tableau.com/v9.0/api/rest_api/en-us/help.htm#REST/rest_api_ref.htm#Query_Sites%3FTocPath%3DAPI%2520Reference%7C_____40 46 | it 'can list sites' do 47 | sites = client.sites.list.to_a 48 | 49 | site = sites.find do |s| 50 | s['contentUrl'] == 'TestSite' 51 | end 52 | 53 | expect(site['id']).to be_a_tableau_id 54 | expect(site).to eq( 55 | 'id' => site['id'], 56 | 'name' => 'TestSite', 57 | 'contentUrl' => 'TestSite', 58 | 'adminMode' => 'ContentAndUsers', 59 | 'state' => 'Active', 60 | 'cacheWarmupEnabled' => 'true', 61 | 'commentingEnabled' => 'true', 62 | 'guestAccessEnabled' => 'false', 63 | 'revisionHistoryEnabled' => 'true', 64 | 'revisionLimit' => '25', 65 | 'subscribeOthersEnabled' => 'true', 66 | 'disableSubscriptions' => 'true' 67 | ) 68 | end 69 | end 70 | 71 | describe '#delete' do 72 | let(:site_client) do 73 | TableauApi.new( 74 | host: ENV.fetch('TABLEAU_HOST', nil), 75 | site_name: 'TestSite2', 76 | username: ENV.fetch('TABLEAU_ADMIN_USERNAME', nil), 77 | password: ENV.fetch('TABLEAU_ADMIN_PASSWORD', nil) 78 | ) 79 | end 80 | 81 | it 'can delete a site' do 82 | site = site_client.sites.list.to_a.find do |s| 83 | s['name'] == 'Test Site 2' 84 | end 85 | expect(site_client.sites.delete(site_id: site['id'])).to be true 86 | end 87 | 88 | it 'raises an error if fails to delete a site' do 89 | expect do 90 | default_client.sites.delete(site_id: 'does-not-exist') 91 | end.to raise_error(TableauApi::TableauError) 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/resources/users_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe TableauApi::Resources::Users, vcr: { cassette_name: 'users' } do 6 | include_context 'tableau client' 7 | 8 | describe '#create' do 9 | # http://onlinehelp.tableau.com/v9.0/api/rest_api/en-us/help.htm#REST/rest_api_ref.htm#Add_User_to_Site%3FTocPath%3DAPI%2520Reference%7C_____7 10 | it 'can create a user in a site' do 11 | user = client.users.create(username: 'test', site_role: 'ExplorerCanPublish') 12 | expect(user['id']).to be_a_tableau_id 13 | expect(user).to eq( 14 | 'id' => user['id'], 15 | 'name' => 'test', 16 | 'siteRole' => 'ExplorerCanPublish', 17 | 'externalAuthUserId' => '', 18 | 'authSetting' => 'ServerDefault' 19 | ) 20 | end 21 | 22 | it 'fails with a bad site_role' do 23 | expect { client.users.create(username: 'test', site_role: 'foo') }.to raise_error('invalid site_role') 24 | end 25 | end 26 | 27 | describe '#list' do 28 | # http://onlinehelp.tableau.com/v9.0/api/rest_api/en-us/help.htm#REST/rest_api_ref.htm#Get_Users_on_Site 29 | it 'can list users in a site' do 30 | sleep(15) if VCR.current_cassette.recording? 31 | user = client.users.list.find do |u| 32 | u['name'] == 'test' 33 | end 34 | expect(user['id']).to be_a_tableau_id 35 | expect(user).to eq('id' => user['id'], 'name' => 'test', 'siteRole' => 'ExplorerCanPublish', 'externalAuthUserId' => '') 36 | end 37 | end 38 | 39 | describe '#update_user' do 40 | # https://onlinehelp.tableau.com/current/api/rest_api/en-us/help.htm#REST/rest_api_ref.htm#Update_User 41 | it 'can change the site role of a user in a site' do 42 | user = client.users.list.find do |u| 43 | u['name'] == 'test' 44 | end 45 | expect(user['siteRole']).to eq('ExplorerCanPublish') 46 | user_after_change = client.users.update_user(user_id: user['id'], site_role: 'Explorer') 47 | expect(user_after_change['siteRole']).to eq('Explorer') 48 | end 49 | 50 | it 'raises an error if the site role is not valid' do 51 | user = client.users.list.find do |u| 52 | u['name'] == 'test' 53 | end 54 | expect do 55 | client.users.update_user(user_id: user['id'], site_role: 'foo') 56 | end.to raise_error(RuntimeError, 'invalid site_role') 57 | end 58 | 59 | it 'raises an error if the user cannot be found' do 60 | expect do 61 | client.users.update_user(user_id: 'foo', site_role: 'Viewer') 62 | end.to raise_error(RuntimeError, 'failed to find user') 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/resources/workbooks_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'tempfile' 5 | require 'chunky_png' 6 | 7 | describe TableauApi::Resources::Workbooks, vcr: { cassette_name: 'workbooks' } do 8 | include_context 'tableau client' 9 | 10 | def find_or_publish_workbook(name) 11 | project = client.projects.list.find { |p| p['name'] == 'test' } 12 | workbook = client.workbooks.list.find { |w| w['name'] == name } 13 | 14 | if workbook 15 | workbook = client.workbooks.find(workbook['id']) 16 | return workbook 17 | end 18 | 19 | client.workbooks.publish( 20 | name: name, 21 | project_id: project['id'], 22 | file: 'spec/fixtures/workbooks/test.twbx', 23 | overwrite: true 24 | ) 25 | end 26 | 27 | # http://onlinehelp.tableau.com/v9.0/api/rest_api/en-us/help.htm#REST/rest_api_ref.htm#Publish_Workbook%3FTocPath%3DAPI%2520Reference%7C_____31 28 | # Workbooks created in a later version of Tableau Desktop cannot be published to earlier versions of Tableau Server. 29 | # - http://kb.tableau.com/articles/knowledgebase/desktop-and-server-compatibility 30 | describe '#publish' do 31 | it 'can publish a twbx workbook in project in a site' do 32 | workbook_name = 'testpublish' 33 | workbook = find_or_publish_workbook(workbook_name) 34 | 35 | expect(workbook['id']).to be_a_tableau_id 36 | expect(workbook['owner']['id']).to be_a_tableau_id 37 | 38 | expect(workbook).to eq( 39 | 'id' => workbook['id'], 40 | 'name' => workbook_name, 41 | 'contentUrl' => workbook_name, 42 | 'showTabs' => 'false', 43 | 'project' => workbook['project'], 44 | 'owner' => workbook['owner'], 45 | 'tags' => nil, 46 | 'views' => workbook['views'], 47 | 'size' => '1', 48 | 'createdAt' => workbook['createdAt'], 49 | 'updatedAt' => workbook['updatedAt'], 50 | 'webpageUrl' => workbook['webpageUrl'] 51 | ) 52 | end 53 | 54 | it 'raises an exception', vcr: { cassette_name: 'workbooks', match_requests_on: %i[path query] } do 55 | ex = expect do 56 | client.workbooks.publish( 57 | name: 'tableau_api test', 58 | project_id: 'foo', 59 | file: 'spec/fixtures/workbooks/test.twbx', 60 | overwrite: true 61 | ) 62 | end 63 | 64 | ex.to raise_error(TableauApi::TableauError) do |e| 65 | expect(e.message).to eq "404005: Resource Not Found; Project 'foo' could not be found." 66 | expect(e.http_response_code).to eq '404' 67 | expect(e.error_code).to eq '404005' 68 | expect(e.summary).to eq 'Resource Not Found' 69 | expect(e.detail).to eq "Project 'foo' could not be found." 70 | end 71 | end 72 | end 73 | 74 | describe '#add_permissions' do 75 | # http://onlinehelp.tableau.com/v9.0/api/rest_api/en-us/help.htm#REST/rest_api_ref.htm#Add_Workbook_Permissions%3FTocPath%3DAPI%2520Reference%7C_____9 76 | it 'can add user permissions to a workbook' do 77 | workbook = find_or_publish_workbook('testpublish') 78 | expect(client.workbooks.add_permissions( 79 | workbook_id: workbook['id'], 80 | user_id: admin_user['id'], 81 | capabilities: { Read: true, ChangePermissions: false } 82 | )).to be true 83 | end 84 | 85 | # http://onlinehelp.tableau.com/v9.0/api/rest_api/en-us/help.htm#REST/rest_api_ref.htm#Add_Workbook_Permissions%3FTocPath%3DAPI%2520Reference%7C_____9 86 | it 'can add group permissions to a workbook' do 87 | workbook = find_or_publish_workbook('testpublish') 88 | expect(client.workbooks.add_permissions( 89 | workbook_id: workbook['id'], 90 | group_id: test_group['id'], 91 | capabilities: { Read: true, ChangePermissions: false } 92 | )).to be true 93 | end 94 | 95 | it 'requires a user or a group id' do 96 | expect do 97 | client.workbooks.add_permissions( 98 | workbook_id: '1', 99 | capabilities: { Read: true } 100 | ) 101 | end.to raise_error(/must specify user_id or group_id/) 102 | end 103 | 104 | it 'does not accept both a user and a group id' do 105 | expect do 106 | client.workbooks.add_permissions( 107 | workbook_id: '1', 108 | user_id: '1', 109 | group_id: '2', 110 | capabilities: { Read: true } 111 | ) 112 | end.to raise_error(/cannot specify user_id and group_id simultaneously/) 113 | end 114 | end 115 | 116 | describe '#delete_permissions' do 117 | it 'can delete a user permission' do 118 | workbook = find_or_publish_workbook('testpublish') 119 | client.workbooks.add_permissions( 120 | workbook_id: workbook['id'], 121 | user_id: admin_user['id'], 122 | capabilities: { Read: true } 123 | ) 124 | expect(client.workbooks.delete_permissions( 125 | workbook_id: workbook['id'], 126 | user_id: admin_user['id'], 127 | capability: 'Read', 128 | capability_mode: 'ALLOW' 129 | )).to be true 130 | end 131 | 132 | it 'can delete a group permission' do 133 | workbook = find_or_publish_workbook('testpublish') 134 | expect(client.workbooks.delete_permissions( 135 | workbook_id: workbook['id'], 136 | group_id: test_group['id'], 137 | capability: 'Read', 138 | capability_mode: 'ALLOW' 139 | )).to be true 140 | end 141 | 142 | it 'accepts a symbol as a permission' do 143 | workbook = find_or_publish_workbook('testpublish') 144 | client.workbooks.add_permissions( 145 | workbook_id: workbook['id'], 146 | group_id: all_users_group['id'], 147 | capabilities: { ExportImage: true } 148 | ) 149 | expect(client.workbooks.delete_permissions( 150 | workbook_id: workbook['id'], 151 | group_id: all_users_group['id'], 152 | capability: :ExportImage, 153 | capability_mode: :ALLOW 154 | )).to be true 155 | end 156 | 157 | it 'requires a user or a group id' do 158 | expect do 159 | client.workbooks.delete_permissions( 160 | workbook_id: '1', 161 | capability: 'READ', 162 | capability_mode: 'ALLOW' 163 | ) 164 | end.to raise_error(/must specify user_id or group_id/) 165 | end 166 | 167 | it 'does not accept both a user and a group id' do 168 | expect do 169 | client.workbooks.delete_permissions( 170 | workbook_id: '1', 171 | user_id: '1', 172 | group_id: '2', 173 | capability: 'READ', 174 | capability_mode: 'ALLOW' 175 | ) 176 | end.to raise_error(/cannot specify user_id and group_id simultaneously/) 177 | end 178 | end 179 | 180 | describe '#permissions' do 181 | it 'can retrieve multiple permissions for a workbook' do 182 | workbook = find_or_publish_workbook('testpublish') 183 | expected = [{ 184 | grantee_type: 'group', 185 | grantee_id: all_users_group['id'], 186 | capabilities: { 187 | Read: true, 188 | ShareView: true, 189 | ViewUnderlyingData: true, 190 | Filter: true, 191 | Write: true 192 | } 193 | }, { 194 | grantee_type: 'group', 195 | grantee_id: test_group['id'], 196 | capabilities: { 197 | ChangePermissions: false 198 | } 199 | }, { 200 | grantee_type: 'user', 201 | grantee_id: admin_user['id'], 202 | capabilities: { 203 | ChangePermissions: false 204 | } 205 | }] 206 | actual = client.workbooks.permissions(workbook_id: workbook['id']) 207 | expect(actual).to eq expected 208 | end 209 | 210 | it 'returns an array for even a single permission' do 211 | workbook = find_or_publish_workbook('testpublish') 212 | 213 | # use up the VCR recording from the previous spec 214 | client.workbooks.permissions(workbook_id: workbook['id']) 215 | 216 | # remove most of the remaining permissions 217 | %w[ExportData ViewComments AddComment].each do |p| 218 | client.workbooks.delete_permissions( 219 | workbook_id: workbook['id'], 220 | group_id: all_users_group['id'], 221 | capability: p, 222 | capability_mode: 'ALLOW' 223 | ) 224 | end 225 | client.workbooks.delete_permissions( 226 | workbook_id: workbook['id'], 227 | group_id: test_group['id'], 228 | capability: :ChangePermissions, 229 | capability_mode: 'DENY' 230 | ) 231 | client.workbooks.delete_permissions( 232 | workbook_id: workbook['id'], 233 | user_id: admin_user['id'], 234 | capability: 'ChangePermissions', 235 | capability_mode: 'DENY' 236 | ) 237 | client.workbooks.delete_permissions( 238 | workbook_id: workbook['id'], 239 | group_id: test_group['id'], 240 | capability: 'ExportImage', 241 | capability_mode: 'ALLOW' 242 | ) 243 | 244 | expected = [{ 245 | grantee_type: 'group', 246 | grantee_id: all_users_group['id'], 247 | capabilities: { 248 | Read: true, 249 | Filter: true, 250 | ShareView: true, 251 | ViewUnderlyingData: true, 252 | Write: true 253 | } 254 | }] 255 | expect(client.workbooks.permissions(workbook_id: workbook['id'])).to eq expected 256 | end 257 | end 258 | 259 | describe '#list' do 260 | it 'can list workbooks' do 261 | workbook = find_or_publish_workbook('testpublish') 262 | 263 | found_workbook = client.workbooks.list.find do |w| 264 | w['contentUrl'] == workbook['contentUrl'] 265 | end 266 | 267 | expect(found_workbook['id']).to be_a_tableau_id 268 | expect(found_workbook).to eq( 269 | 'id' => workbook['id'], 270 | 'name' => workbook['name'], 271 | 'contentUrl' => workbook['contentUrl'], 272 | 'showTabs' => 'false', 273 | 'project' => workbook['project'], 274 | 'owner' => workbook['owner'], 275 | 'tags' => nil, 276 | 'size' => '1', 277 | 'updatedAt' => workbook['updatedAt'], 278 | 'createdAt' => workbook['createdAt'], 279 | 'webpageUrl' => workbook['webpageUrl'] 280 | ) 281 | 282 | same_workbook = client.workbooks.list.find do |w| 283 | w['name'] == workbook['name'] 284 | end 285 | 286 | expect(found_workbook).to eq same_workbook 287 | end 288 | 289 | it 'can paginate workbooks' do 290 | find_or_publish_workbook('test pagination1') 291 | find_or_publish_workbook('test pagination2') 292 | find_or_publish_workbook('test pagination3') 293 | workbooks = client.workbooks.list 294 | 295 | url = "sites/#{client.auth.site_id}/users/#{client.auth.user_id}/workbooks" 296 | expect(client.connection.api_get_collection(url, 'workbooks.workbook', page_size: 2).count).to eq workbooks.count 297 | end 298 | end 299 | 300 | # http://onlinehelp.tableau.com/v9.0/api/rest_api/en-us/help.htm#REST/rest_api_ref.htm#Update_Workbook%3FTocPath%3DAPI%2520Reference%7C_____59 301 | describe '#update' do 302 | it 'can update the workbook' do 303 | workbook = find_or_publish_workbook('testpublish') 304 | expect(client.workbooks.update( 305 | workbook_id: workbook['id'], 306 | owner_user_id: admin_user['id'] 307 | )).to be true 308 | end 309 | end 310 | 311 | describe '#views' do 312 | it 'can get the list of views for a workbook' do 313 | workbook = find_or_publish_workbook('testpublish') 314 | views = client.workbooks.views(workbook['id']).to_a 315 | expect(views).to eq([workbook['views']['view']]) 316 | end 317 | end 318 | 319 | describe '#find' do 320 | it 'can find a workbook by workbook_id' do 321 | workbook = find_or_publish_workbook('testpublish') 322 | found_workbook = client.workbooks.find(workbook['id']) 323 | 324 | expect(workbook).to eq found_workbook 325 | end 326 | end 327 | 328 | describe '#preview_image' do 329 | it 'can download a preview image' do 330 | workbook = find_or_publish_workbook('testpublish') 331 | res = client.workbooks.preview_image(workbook_id: workbook['id']) 332 | f = Tempfile.new('png') 333 | f.write(res) 334 | f.close 335 | # will raise an error if PNG parsing fails 336 | ChunkyPNG::Image.from_file(f) 337 | end 338 | end 339 | 340 | describe '#refresh' do 341 | it 'can refresh a workbook' do 342 | workbook = find_or_publish_workbook('testpublish') 343 | expect( 344 | client.workbooks.refresh(workbook_id: workbook['id']) 345 | ).to be true 346 | end 347 | end 348 | 349 | describe '#version' do 350 | it 'can get the version of a twbx file' do 351 | expect(client.workbooks.version('spec/fixtures/workbooks/test.twbx')).to eq '10.5' 352 | end 353 | 354 | it 'returns nil if file not found' do 355 | expect(client.workbooks.version('spec/fixtures/workbooks/foo.twbx')).to be_nil 356 | end 357 | 358 | it 'returns nil if file not twbx' do 359 | expect(client.workbooks.version('spec/fixtures/workbooks/foo.bar')).to be_nil 360 | end 361 | end 362 | 363 | def test_group 364 | @test_group ||= client.groups.list.find { |g| g['name'] == 'testgroup' } 365 | end 366 | 367 | def all_users_group 368 | @all_users_group ||= client.groups.list.find { |g| g['name'] == 'All Users' } 369 | end 370 | 371 | def admin_user 372 | @admin_user ||= client.users.list.find { |g| g['name'] == ENV['TABLEAU_ADMIN_USERNAME'] } 373 | end 374 | end 375 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path('../lib', __dir__) 4 | require 'tableau_api' 5 | 6 | require 'pry' 7 | 8 | require 'vcr' 9 | 10 | if ENV['TABLEAU_ADMIN_USERNAME'].nil? || ENV['TABLEAU_ADMIN_PASSWORD'].nil? 11 | puts 'TABLEAU_ADMIN_USERNAME and TABLEAU_ADMIN_PASSWORD must be set to record new VCR cassettes' 12 | 13 | ENV['TABLEAU_ADMIN_USERNAME'] = 'FakeTableauAdminUsername' 14 | ENV['TABLEAU_ADMIN_PASSWORD'] = 'FakeTableauAdminPassword' 15 | end 16 | 17 | ENV['TABLEAU_HOST'] = 'http://localhost:2000' if ENV['TABLEAU_HOST'].nil? 18 | 19 | VCR.configure do |config| 20 | config.cassette_library_dir = 'spec/fixtures/vcr_cassettes' 21 | config.hook_into :webmock 22 | 23 | config.default_cassette_options = { record: :new_episodes, match_requests_on: %i[path method body query] } 24 | 25 | config.filter_sensitive_data('TABLEAU_ADMIN_USERNAME') { ENV.fetch('TABLEAU_ADMIN_USERNAME', nil) } 26 | config.filter_sensitive_data('TABLEAU_ADMIN_PASSWORD') { ENV['TABLEAU_ADMIN_PASSWORD'].encode(xml: :text) } 27 | config.filter_sensitive_data('http://TABLEAU_HOST') { ENV.fetch('TABLEAU_HOST', nil) } 28 | 29 | config.allow_http_connections_when_no_cassette = false 30 | 31 | # only record a whitelist of test site and user elements 32 | config.before_record do |interaction| 33 | response = interaction.response 34 | elements = response.body.scan(/<(?:site|user)\s[^>]+name[^>]+>/) 35 | sensitive_elements = elements.reject { |e| e.match(/"(Default|TestSite|Test Site 2|test|test_test|TABLEAU_ADMIN_USERNAME)"/) } 36 | unless sensitive_elements.empty? 37 | sensitive_elements.each { |e| response.body.gsub! e, '' } 38 | response.body.gsub!(/totalAvailable="\d+"/, "totalAvailable=\"#{elements.length - sensitive_elements.length}\"") 39 | end 40 | raise 'Cassette might contain sensitive data; does a regex need to be updated?' if response.body =~ /civis/i 41 | end 42 | config.configure_rspec_metadata! 43 | end 44 | 45 | RSpec.configure do |config| 46 | # Turn deprecation warnings into errors. 47 | config.raise_errors_for_deprecations! 48 | 49 | # Persist example state. Enables --only-failures: 50 | # http://rspec.info/blog/2015/06/rspec-3-3-has-been-released/#core-new---only-failures-option 51 | config.example_status_persistence_file_path = 'tmp/examples.txt' 52 | config.run_all_when_everything_filtered = true 53 | end 54 | 55 | RSpec::Matchers.define :be_a_tableau_id do 56 | match do |actual| 57 | actual.match(/\A\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\z/) 58 | end 59 | end 60 | 61 | RSpec::Matchers.define :be_a_token do 62 | match do |actual| 63 | actual.match(/\A\w{32}\z/) || actual.match(/\A[\w-]{22}\|\w{32}\z/) 64 | end 65 | end 66 | 67 | RSpec::Matchers.define :be_a_trusted_ticket do 68 | match do |actual| 69 | actual.match(/\A[\w-]{24}\z/) || actual.match(/\A[\w\-=]{24}:[\w-]{24}\z/) 70 | end 71 | end 72 | 73 | RSpec.shared_context 'tableau client' do 74 | let(:client) do 75 | TableauApi.new( 76 | host: ENV.fetch('TABLEAU_HOST', nil), 77 | site_name: 'TestSite', 78 | username: ENV.fetch('TABLEAU_ADMIN_USERNAME', nil), 79 | password: ENV.fetch('TABLEAU_ADMIN_PASSWORD', nil) 80 | ) 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/tableau_api_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe TableauApi do 6 | it 'has a version number' do 7 | expect(TableauApi::VERSION).not_to be nil 8 | end 9 | 10 | it 'can create a client from TableauApi' do 11 | expect(TableauApi.new(host: 'tableau.domain.tld', site_name: 'Default', username: 'ExampleUsername')).to be_an_instance_of(TableauApi::Client) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /tableau_api.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'tableau_api/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'tableau_api' 9 | spec.version = TableauApi::VERSION 10 | spec.authors = ['Christopher Manning', 'Matt Brennan', 11 | 'Jonathan Cobian'] 12 | spec.email = ['opensource@civisanalytics.com'] 13 | 14 | spec.summary = 'Ruby interface to the Tableau API.' 15 | spec.homepage = 'https://github.com/civisanalytics/tableau_api' 16 | spec.license = 'BSD-3-Clause' 17 | 18 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 19 | spec.bindir = 'exe' 20 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 21 | spec.require_paths = ['lib'] 22 | spec.required_ruby_version = '>= 2.7' 23 | 24 | spec.add_dependency 'builder', '~> 3.2' 25 | spec.add_dependency 'httparty', '~> 0.13' 26 | spec.add_dependency 'multipart-post', '~> 2.0' 27 | spec.add_dependency 'rubyzip', '~> 1.0' 28 | 29 | spec.add_development_dependency 'chunky_png', '~> 1.3.11' 30 | spec.add_development_dependency 'pry', '~> 0.10' 31 | spec.add_development_dependency 'pry-byebug', '~> 3.4' 32 | spec.add_development_dependency 'rake', '~> 12.3.3' 33 | spec.add_development_dependency 'rspec', '~> 3.6' 34 | spec.add_development_dependency 'rubocop', '~> 1.48.1' 35 | spec.add_development_dependency 'vcr', '~> 6.0' 36 | spec.add_development_dependency 'webmock', '~> 3.0' 37 | spec.metadata['rubygems_mfa_required'] = 'true' 38 | end 39 | --------------------------------------------------------------------------------