From 45d4224bb51219f8d6658dd0535cce59f0a0f174 Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Wed, 21 Jul 2021 13:55:30 +1000 Subject: [PATCH 01/29] refactor: helper for changes on a model --- .../controllers/application.cr | 32 +++++++++++++++++++ src/placeos-rest-api/controllers/drivers.cr | 31 ++---------------- .../controllers/repositories.cr | 29 ++--------------- 3 files changed, 36 insertions(+), 56 deletions(-) diff --git a/src/placeos-rest-api/controllers/application.cr b/src/placeos-rest-api/controllers/application.cr index cf7b8d17..3cab7161 100644 --- a/src/placeos-rest-api/controllers/application.cr +++ b/src/placeos-rest-api/controllers/application.cr @@ -99,6 +99,38 @@ module PlaceOS::Api body end + # Query helper + ########################################################################### + + # Helper for observing change on a row + # + def self.find_change(model : T, timeout : Time::Span = 3.minutes, &block : T -> Bool) : T? forall T + changefeed = T.changes(model.id.as(String)) + channel = Channel(T?).new(1) + begin + spawn do + update_event = changefeed.find do |event| + block.call(event.value) + end + channel.send(update_event.try &.value) + end + + select + when received = channel.receive? + received + when timeout(timeout) + Log.info { "timeout for waiting for a change on #{T.name}<#{model.id}>" } + nil + end + rescue + nil + ensure + # Terminate the changefeed + changefeed.stop + channel.close + end + end + # Error Handlers ########################################################################### diff --git a/src/placeos-rest-api/controllers/drivers.cr b/src/placeos-rest-api/controllers/drivers.cr index 3cb6ea72..e066ec43 100644 --- a/src/placeos-rest-api/controllers/drivers.cr +++ b/src/placeos-rest-api/controllers/drivers.cr @@ -86,36 +86,9 @@ module PlaceOS::Api driver.update_fields(commit: "RECOMPILE-#{driver.commit}") # Initiate changefeed on the document's commit - changefeed = Model::Driver.changes(driver.id.as(String)) - channel = Channel(Model::Driver?).new(1) - - # Wait until the commit hash is not head with a timeout of 20 seconds - found_driver = begin - spawn do - update_event = changefeed.find do |event| - driver_update = event.value - driver_update.destroyed? || !driver_update.commit.starts_with? "RECOMPILE" - end - channel.send(update_event.try &.value) - end - - select - when received = channel.receive - received - when timeout(20.seconds) - raise "timeout waiting for recompile" - end - - received - rescue - nil - ensure - channel.close - # Terminate the changefeed - changefeed.stop + find_change(driver) do |driver_update| + driver_update.destroyed? || !driver_update.commit.starts_with? "RECOMPILE" end - - found_driver end # Check if the core responsible for the driver has finished compilation diff --git a/src/placeos-rest-api/controllers/repositories.cr b/src/placeos-rest-api/controllers/repositories.cr index 3ca46e29..1f3a8058 100644 --- a/src/placeos-rest-api/controllers/repositories.cr +++ b/src/placeos-rest-api/controllers/repositories.cr @@ -76,33 +76,8 @@ module PlaceOS::Api # Trigger a pull event repository.pull! - # Initiate changefeed on the document's commit_hash - changefeed = Model::Repository.changes(repository.id.as(String)) - channel = Channel(Model::Repository?).new(1) - - # Wait until the commit hash is not head with a timeout of 20 seconds - found_repo = begin - spawn do - update_event = changefeed.find do |event| - repo = event.value - repo.destroyed? || !repo.should_pull? - end - channel.send(update_event.try &.value) - end - - select - when received = channel.receive? - received - when timeout(3.minutes) - Log.info { "timeout" } - raise "timeout for repository update" - end - rescue - nil - ensure - # Terminate the changefeed - changefeed.stop - channel.close + found_repo = find_change(repository) do |repo| + repo.destroyed? || !repo.should_pull? end unless found_repo.nil? From 54de22725b12199f2ee178b0aa5d06d688be7ce7 Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Mon, 9 Aug 2021 14:22:16 +1000 Subject: [PATCH 02/29] feat(controller:repositories): use build client to fetch driver commits --- docker-compose.yml | 13 ++++++++ shard.lock | 32 +++++++++++++++++++ shard.yml | 4 +++ src/constants.cr | 22 +++++++++---- .../controllers/repositories.cr | 32 +++++++++++-------- src/placeos-rest-api/controllers/root.cr | 2 +- src/placeos-rest-api/controllers/webhook.cr | 2 +- 7 files changed, 85 insertions(+), 22 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7e542a34..774208c5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,10 @@ x-deployment-env: &deployment-env SG_ENV: ${SG_ENV:-development} TZ: $TZ +x-build-client-env: &build-client-env + PLACEOS_BUILD_HOST: ${PLACEOS_BUILD_HOST:-build} + PLACEOS_BUILD_PORT: ${PLACEOS_BUILD_PORT:-3000} + x-elastic-client-env: &elastic-client-env ES_HOST: ${ELASTIC_HOST:-elastic} ES_PORT: ${ELASTIC_PORT:-9200} @@ -47,11 +51,20 @@ services: GITHUB_ACTION: ${GITHUB_ACTION:-} <<: *deployment-env # Service Hosts + <<: *build-client-env <<: *elastic-client-env <<: *etcd-client-env <<: *redis-client-env <<: *rethinkdb-client-env + # TODO: Mock and remove after interface has stabilised + build: + image: placeos/build:${PLACE_BUILD_TAG:-nightly} + restart: always + hostname: build + environment: + <<: *deployment-env + elastic: image: blacktop/elasticsearch:${ELASTIC_VERSION:-7.6} restart: always diff --git a/shard.lock b/shard.lock index 088159a5..fc01971d 100644 --- a/shard.lock +++ b/shard.lock @@ -21,6 +21,14 @@ shards: git: https://github.com/sija/any_hash.cr.git version: 0.2.5 + awscr-s3: + git: https://github.com/taylorfinnell/awscr-s3.git + version: 0.8.3+git.commit.559518424669934ee6390541a6d88262ef75a370 + + awscr-signer: + git: https://github.com/taylorfinnell/awscr-signer.git + version: 0.8.2 + backtracer: git: https://github.com/sija/backtracer.cr.git version: 1.2.1 @@ -29,6 +37,10 @@ shards: git: https://github.com/spider-gazelle/bindata.git version: 1.9.1 + clip: + git: https://github.com/erdnaxeli/clip.git + version: 0.2.4 + clustering: git: https://github.com/place-labs/clustering.git version: 3.1.1 @@ -109,6 +121,10 @@ shards: git: https://github.com/luckyframework/lucky_router.git version: 0.5.0 + molinillo: + git: https://github.com/crystal-lang/crystal-molinillo.git + version: 0.2.0 + murmur3: git: https://github.com/aca-labs/murmur3.git version: 0.1.1+git.commit.7cbe25c0ca8d052c9d98c377c824dcb0e038c790 @@ -117,6 +133,14 @@ shards: git: https://github.com/place-labs/neuroplastic.git version: 1.8.0 + open_api: + git: https://github.com/elbywan/open_api.cr.git + version: 1.3.0 + + openapi-generator: + git: https://github.com/place-labs/openapi-generator.git + version: 2.1.0+git.commit.d925772ae1f36c51c0665d743bba4ac6ca9466a8 + openssl_ext: git: https://github.com/spider-gazelle/openssl_ext.git version: 2.1.5 @@ -125,6 +149,10 @@ shards: git: https://github.com/spider-gazelle/pinger.git version: 1.1.2 + placeos-build: + git: https://github.com/placeos/build.git + version: 0.3.0 + placeos-compiler: git: https://github.com/placeos/compiler.git version: 4.4.1 @@ -205,6 +233,10 @@ shards: git: https://github.com/spider-gazelle/secrets-env.git version: 1.3.1 + shards: + git: https://github.com/crystal-lang/shards.git + version: 0.15.0 + simple_retry: git: https://github.com/spider-gazelle/simple_retry.git version: 1.1.1 diff --git a/shard.yml b/shard.yml index 2afe1fd6..0e2637c7 100644 --- a/shard.yml +++ b/shard.yml @@ -42,6 +42,10 @@ dependencies: github: spider-gazelle/pinger version: ~> 1 + # For build client + placeos-build: + github: placeos/build + # For core client placeos-core: github: placeos/core diff --git a/src/constants.cr b/src/constants.cr index dada7609..c29b2fa1 100644 --- a/src/constants.cr +++ b/src/constants.cr @@ -9,17 +9,25 @@ module PlaceOS::Api CORE_NAMESPACE = "core" + PROD = ENV["SG_ENV"]?.try(&.downcase) == "production" + + # Service Configuration + ETCD_HOST = ENV["ETCD_HOST"]? || "localhost" ETCD_PORT = (ENV["ETCD_PORT"]? || "2379").to_i - PLACE_DISPATCH_HOST = ENV["PLACE_DISPATCH_HOST"]? || "dispatch" - PLACE_DISPATCH_PORT = (ENV["PLACE_DISPATCH_PORT"]? || "3000").to_i + # PlaceOS Service Configuration - PLACE_SOURCE_HOST = ENV["PLACE_SOURCE_HOST"]? || "127.0.0.1" - PLACE_SOURCE_PORT = (ENV["PLACE_SOURCE_PORT"]? || 3000).to_i + PLACE_DISPATCH_HOST = (ENV["PLACEOS_DISPATCH_HOST"]? || ENV["PLACE_DISPATCH_HOST"]?).presence || "dispatch" + PLACE_DISPATCH_PORT = (ENV["PLACEOS_DISPATCH_PORT"]? || ENV["PLACE_DISPATCH_PORT"]?).presence.try &.to_i || 3000 - # server defaults in `./app.cr` - TRIGGERS_URI = URI.parse(ENV["TRIGGERS_URI"]? || "http://triggers:3000") + PLACE_SOURCE_HOST = (ENV["PLACEOS_SOURCE_HOST"]? || ENV["PLACE_SOURCE_HOST"]?).presence || "source" + PLACE_SOURCE_PORT = (ENV["PLACEOS_SOURCE_PORT"]? || ENV["PLACE_SOURCE_PORT"]?).presence.try &.to_i || 3000 - PROD = ENV["SG_ENV"]?.try(&.downcase) == "production" + PLACE_BUILD_HOST = ENV["PLACEOS_BUILD_HOST"]?.presence || "build" + PLACE_BUILD_PORT = ENV["PLACEOS_BUILD_PORT"]?.presence.try &.to_i? || 3000 + + PLACE_BUILD_URI = URI.parse("http://#{BUILD_HOST}:#{BUILD_PORT}") + + PLACE_TRIGGERS_URI = URI.parse(ENV["PLACEOS_TRIGGERS_URI"]?.presence || ENV["TRIGGERS_URI"]?.presence || "http://triggers:3000") end diff --git a/src/placeos-rest-api/controllers/repositories.cr b/src/placeos-rest-api/controllers/repositories.cr index 1f3a8058..fb013cb0 100644 --- a/src/placeos-rest-api/controllers/repositories.cr +++ b/src/placeos-rest-api/controllers/repositories.cr @@ -1,3 +1,4 @@ +require "placeos-build/client" require "placeos-frontends/client" require "./application" @@ -109,30 +110,37 @@ module PlaceOS::Api end get "/:id/commits", :commits do - number_of_commits = params["limit"]?.try &.to_i + limit = params["limit"]?.try &.to_i file_name = params["driver"]? commits = Api::Repositories.commits( repository: current_repo, request_id: request_id, - number_of_commits: number_of_commits, + limit: limit, file_name: file_name, ) render json: commits end - def self.commits(repository : Model::Repository, request_id : String, number_of_commits : Int32? = nil, file_name : String? = nil) - number_of_commits = 50 if number_of_commits.nil? - if repository.repo_type == Model::Repository::Type::Driver - # Dial the core responsible for the driver - Api::Systems.core_for(repository.folder_name, request_id) do |core_client| - core_client.driver(file_name || ".", repository.folder_name, number_of_commits) + def self.commits(repository : Model::Repository, request_id : String, file_name : String? = nil, branch : String? = nil, limit : Int32? = nil) + limit = 50 if limit.nil? + branch = "master" if branch.nil? + + case repository.repo_type + in .driver? + Build::Client.client do |client| + args = {url: repository.uri, request_id: request_id, count: limit, branch: branch, username: repository.username, password: repository.password} + if file_name + client.file_commits(**args.merge({file: file_name})) + else + client.repository_commits(**args) + end end - else + in .interface? # Dial the frontends service Frontends::Client.client(request_id: request_id) do |frontends_client| - frontends_client.commits(repository.folder_name, number_of_commits) + frontends_client.commits(repository.folder_name, limit) end end end @@ -170,9 +178,7 @@ module PlaceOS::Api frontends_client.branches(repository.folder_name) end in .driver? - Api::Systems.core_for(repository.id.as(String), request_id) do |core_client| - core_client.branches?(repository.folder_name) - end + Build::Client.client &.branches(url: repository.uri, request_id: request_id, username: repository.username, password: repository.password) end.tap do |result| if result.nil? Log.info { { diff --git a/src/placeos-rest-api/controllers/root.cr b/src/placeos-rest-api/controllers/root.cr index c63fe243..e63646ea 100644 --- a/src/placeos-rest-api/controllers/root.cr +++ b/src/placeos-rest-api/controllers/root.cr @@ -121,7 +121,7 @@ module PlaceOS::Api end protected def self.triggers_version : PlaceOS::Model::Version - trigger_uri = TRIGGERS_URI.dup + trigger_uri = PLACE_TRIGGERS_URI.dup trigger_uri.path = "/api/triggers/v2/version" response = HTTP::Client.get trigger_uri PlaceOS::Model::Version.from_json(response.body) diff --git a/src/placeos-rest-api/controllers/webhook.cr b/src/placeos-rest-api/controllers/webhook.cr index 1abb20c2..633af8c6 100644 --- a/src/placeos-rest-api/controllers/webhook.cr +++ b/src/placeos-rest-api/controllers/webhook.cr @@ -22,7 +22,7 @@ module PlaceOS::Api def notify(method_type : String) # ameba:disable Metrics/CyclomaticComplexity # Notify the trigger service # TODO: Triggers service should expose a versioned client - trigger_uri = TRIGGERS_URI.dup + trigger_uri = PLACE_TRIGGERS_URI.dup trigger_uri.path = "/api/triggers/v2/webhook?id=#{current_trigger_instance.id}&secret=#{current_trigger_instance.webhook_secret}" trigger_response = HTTP::Client.post( trigger_uri, From 35f149a7c81516577d667f80884ad6bdef1a8578 Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Mon, 9 Aug 2021 15:02:54 +1000 Subject: [PATCH 03/29] refactor(controller:repositories): use build for driver metadata --- .../controllers/repositories.cr | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/placeos-rest-api/controllers/repositories.cr b/src/placeos-rest-api/controllers/repositories.cr index fb013cb0..f31672c5 100644 --- a/src/placeos-rest-api/controllers/repositories.cr +++ b/src/placeos-rest-api/controllers/repositories.cr @@ -10,14 +10,14 @@ module PlaceOS::Api before_action :check_admin, except: [:index, :show] before_action :check_support, only: [:index, :show] - before_action :current_repo, only: [:branches, :commits, :destroy, :details, :drivers, :show, :update, :update_alt] + before_action :current_repository, only: [:branches, :commits, :destroy, :details, :drivers, :show, :update, :update_alt] before_action :body, only: [:create, :update, :update_alt] before_action :drivers_only, only: [:drivers, :details] - getter current_repo : Model::Repository { find_repo } + getter current_repository : Model::Repository { find_repo } private def drivers_only - unless current_repo.repo_type.driver? + unless current_repository.repo_type.driver? render_error(:bad_request, "not a driver repository") end end @@ -30,18 +30,18 @@ module PlaceOS::Api end def show - render json: current_repo + render json: current_repository end def update - current_repo.assign_attributes_from_json(self.body) + current_repository.assign_attributes_from_json(self.body) # Must destroy and re-add to change driver repository URIs - if current_repo.uri_changed? && current_repo.repo_type.driver? + if current_repository.uri_changed? && current_repository.repo_type.driver? return render_error(HTTP::Status::UNPROCESSABLE_ENTITY, "`uri` of Driver repositories cannot change") end - save_and_respond current_repo + save_and_respond current_repository end # TODO: replace manual id with interpolated value from `id_param` @@ -52,12 +52,12 @@ module PlaceOS::Api end def destroy - current_repo.destroy + current_repository.destroy head :ok end post "/:id/pull", :pull do - result = Repositories.pull_repository(current_repo) + result = Repositories.pull_repository(current_repository) if result destroyed, commit_hash = result if destroyed @@ -97,7 +97,7 @@ module PlaceOS::Api end get "/:id/drivers", :drivers do - repository_folder = current_repo.folder_name + repository_folder = current_repository.folder_name # Request to core: # "/api/core/v1/drivers/?repository=#{repository}" @@ -114,7 +114,7 @@ module PlaceOS::Api file_name = params["driver"]? commits = Api::Repositories.commits( - repository: current_repo, + repository: current_repository, request_id: request_id, limit: limit, file_name: file_name, @@ -149,21 +149,23 @@ module PlaceOS::Api driver = params["driver"] commit = params["commit"] - # Request to core: - # "/api/core/v1/drivers/#{file_name}/details?repository=#{repository}&count=#{number_of_commits}" - # Returns: https://github.com/placeos/driver/blob/master/docs/command_line_options.md#discovery-and-defaults - details = Api::Systems.core_for(driver, request_id) do |core_client| - core_client.driver_details(driver, commit, current_repo.folder_name) + info = Build::Client.client do |client| + client.metadata( + file: driver, + url: current_repository.uri, + commit: commit, + username: current_repository.username, + password: current_repository.password, + request_id: request_id, + ) end - # The raw JSON string is returned - response.headers["Content-Type"] = "application/json" - render text: details + render json: info end get "/:id/branches", :branches do branches = Api::Repositories.branches( - repository: current_repo, + repository: current_repository, request_id: request_id, ) From bef269b47204858da92a9684eae9c96c270f11fa Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Tue, 10 Aug 2021 18:52:14 +1000 Subject: [PATCH 04/29] build(shard.yml): reference build branch --- shard.lock | 2 +- shard.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/shard.lock b/shard.lock index fc01971d..230339c8 100644 --- a/shard.lock +++ b/shard.lock @@ -151,7 +151,7 @@ shards: placeos-build: git: https://github.com/placeos/build.git - version: 0.3.0 + version: 0.3.0+git.commit.0388aaece63a784c2692ed650ca471754421b99d placeos-compiler: git: https://github.com/placeos/compiler.git diff --git a/shard.yml b/shard.yml index 0e2637c7..02cab3de 100644 --- a/shard.yml +++ b/shard.yml @@ -45,6 +45,7 @@ dependencies: # For build client placeos-build: github: placeos/build + branch: feat/repositories/discovery # For core client placeos-core: From 8c67512b2aa61e9ea7eb10e175fcca25ec8651b4 Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Wed, 11 Aug 2021 11:04:04 +1000 Subject: [PATCH 05/29] test(api:repositories): test for commit listing --- shard.lock | 2 +- shard.yml | 4 ++-- spec/repositories_spec.cr | 44 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/shard.lock b/shard.lock index 230339c8..f16fbfc9 100644 --- a/shard.lock +++ b/shard.lock @@ -151,7 +151,7 @@ shards: placeos-build: git: https://github.com/placeos/build.git - version: 0.3.0+git.commit.0388aaece63a784c2692ed650ca471754421b99d + version: 0.4.0 placeos-compiler: git: https://github.com/placeos/compiler.git diff --git a/shard.yml b/shard.yml index 02cab3de..958c35b8 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: placeos-rest-api -version: 1.29.3 +version: 1.30.0 crystal: ~> 1 targets: @@ -45,7 +45,7 @@ dependencies: # For build client placeos-build: github: placeos/build - branch: feat/repositories/discovery + version: ~> 0.4 # For core client placeos-core: diff --git a/spec/repositories_spec.cr b/spec/repositories_spec.cr index da24a60a..12c61aee 100644 --- a/spec/repositories_spec.cr +++ b/spec/repositories_spec.cr @@ -69,9 +69,51 @@ module PlaceOS::Api end end + describe "GET /:id/commits" do + context "interface" do + pending "fetches the commits for a repository" do + end + end + + context "driver" do + repo = Model::Generator.repository(type: :driver) + repo.uri = "https://github.com/placeOS/private-drivers" + repo.save! + + it "fetches the commits for a repository" do + id = repo.id.as(String) + path = "#{base}#{id}/commits" + result = curl( + method: "GET", + path: path, + headers: authorization_header.merge({"Content-Type" => "application/json"}), + ) + + result.status.should eq HTTP::Status::OK + Array(String).from_json(result.body).should_not be_empty + end + + it "fetches the commits for a file" do + id = repo.id.as(String) + params = HTTP::Params{ + "driver" => "drivers/place/private_helper.cr", + } + path = "#{base}#{id}/commits?#{params}" + result = curl( + method: "GET", + path: path, + headers: authorization_header.merge({"Content-Type" => "application/json"}), + ) + + result.status.should eq HTTP::Status::OK + Array(String).from_json(result.body).should_not be_empty + end + end + end + describe "driver only actions" do it "errors if enumerating drivers in an interface repo" do - repository = Model::Generator.repository(type: Model::Repository::Type::Interface).save! + repository = Model::Generator.repository(type: :interface).save! id = repository.id.as(String) path = "#{base}#{id}/drivers" From 678a28730c9778bfaa141f13a56fac3a25015812 Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Wed, 11 Aug 2021 18:45:34 +1000 Subject: [PATCH 06/29] refactor(api:repositories): use build for driver discovery --- src/placeos-rest-api/controllers/repositories.cr | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/placeos-rest-api/controllers/repositories.cr b/src/placeos-rest-api/controllers/repositories.cr index f31672c5..6968cb31 100644 --- a/src/placeos-rest-api/controllers/repositories.cr +++ b/src/placeos-rest-api/controllers/repositories.cr @@ -97,14 +97,14 @@ module PlaceOS::Api end get "/:id/drivers", :drivers do - repository_folder = current_repository.folder_name - - # Request to core: - # "/api/core/v1/drivers/?repository=#{repository}" - # Returns: `["path/to/file.cr"]` - drivers = Api::Systems.core_for(repository_folder, request_id) do |core_client| - core_client.drivers(repository_folder) - end + drivers = Build::Client.client &.discover_drivers( + url: current_repository.uri, + commit: current_repository.commit_hash, + branch: current_repository.branch, + username: current_repository.username, + password: current_repository.decrypt_password, + request_id: request_id, + ) render json: drivers end From aa458f7f72847bbbce18b3e8f0f1f0ee5ae1af86 Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Wed, 11 Aug 2021 19:02:28 +1000 Subject: [PATCH 07/29] refactor(api:drivers): use build for driver compilation status --- spec/modules_spec.cr | 9 ---- src/placeos-rest-api/controllers/drivers.cr | 59 ++++++--------------- src/placeos-rest-api/controllers/modules.cr | 7 +-- 3 files changed, 16 insertions(+), 59 deletions(-) diff --git a/spec/modules_spec.cr b/spec/modules_spec.cr index 35b96af7..7d956e58 100644 --- a/spec/modules_spec.cr +++ b/spec/modules_spec.cr @@ -1,15 +1,6 @@ require "./helper" require "timecop" -module PlaceOS - class Api::Modules - # Mock a stateful request to Core made by Api::Modules - def self.driver_compiled?(mod : Model::Module, request_id : String) - true - end - end -end - module PlaceOS::Api describe Modules do # ameba:disable Lint/UselessAssign diff --git a/src/placeos-rest-api/controllers/drivers.cr b/src/placeos-rest-api/controllers/drivers.cr index e066ec43..43161347 100644 --- a/src/placeos-rest-api/controllers/drivers.cr +++ b/src/placeos-rest-api/controllers/drivers.cr @@ -38,7 +38,7 @@ module PlaceOS::Api include_compilation_status = !params.has_key?("compilation_status") || params["compilation_status"] != "false" result = !include_compilation_status ? current_driver : with_fields(current_driver, { - :compilation_status => Api::Drivers.compilation_status(current_driver, request_id), + :compilation_status => Api::Drivers.driver_compiled?(current_driver, request_id), }) render json: result @@ -91,15 +91,10 @@ module PlaceOS::Api end end - # Check if the core responsible for the driver has finished compilation + # Check if build has finished compilation of the driver # get("/:id/compiled", :compiled) do - if (repository = current_driver.repository).nil? - Log.error { {repository_id: current_driver.repository_id, message: "failed to load driver's repository"} } - head :internal_server_error - end - - compiled = self.class.driver_compiled?(current_driver, repository, request_id) + compiled = self.class.driver_compiled?(current_driver, request_id) Log.info { "#{compiled ? "" : "not "}compiled" } @@ -117,46 +112,22 @@ module PlaceOS::Api end end - def self.driver_compiled?(driver : Model::Driver, repository : Model::Repository, request_id : String, key : String? = nil) : Bool - Api::Systems.core_for(key.presence || driver.file_name, request_id) do |core_client| - core_client.driver_compiled?( - file_name: URI.encode(driver.file_name), - repository: repository.folder_name, - commit: driver.commit, - tag: driver.id.as(String), - ) - end + def self.driver_compiled?(driver : Model::Driver, request_id : String) : Bool + repository = driver.repository.not_nil! + + !!Build::Client.client &.compiled( + file: driver.file_name, + url: repository.uri, + commit: driver.commit, + username: repository.username, + password: repository.decrypt_password, + request_id: request_id, + ) rescue e - Log.error(exception: e) { "failed to request driver compilation status from core" } + Log.error(exception: e) { "failed to request driver compilation status from build" } false end - # Returns the compilation status of a driver across the cluster - def self.compilation_status( - driver : Model::Driver, - request_id : String? = "migrate to Log" - ) : Hash(String, Bool) - tag = driver.id.as(String) - repository_folder = driver.repository!.folder_name - - nodes = core_discovery.node_hash - result = Promise.all(nodes.map { |name, uri| - Promise.defer { - status = begin - Core::Client.client(uri, request_id) { |client| - client.driver_compiled?(driver.file_name, driver.commit, repository_folder, tag) - } - rescue e - Log.error(exception: e) { "failed to request compilation status from core" } - false - end - {name, status} - } - }).get - - result.to_h - end - # Helpers ########################################################################### diff --git a/src/placeos-rest-api/controllers/modules.cr b/src/placeos-rest-api/controllers/modules.cr index a4278553..cc5b7fba 100644 --- a/src/placeos-rest-api/controllers/modules.cr +++ b/src/placeos-rest-api/controllers/modules.cr @@ -277,12 +277,7 @@ module PlaceOS::Api return false end - if (repository = driver.repository).nil? - Log.error { "failed to load Driver<#{driver.id}>'s Repository<#{driver.repository_id}>" } - return false - end - - Api::Drivers.driver_compiled?(driver, repository, request_id, mod.id.as(String)) + Api::Drivers.driver_compiled?(driver, request_id) end def self.module_state(mod : Model::Module | String, key : String? = nil) From e6aad2771e0ee8cdfd0c1bd85950cf1df4aec77d Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Tue, 17 Aug 2021 20:10:32 +1000 Subject: [PATCH 08/29] chore(models): update to new scopes --- shard.lock | 8 ++++---- shard.yml | 2 +- spec/helper.cr | 4 ++-- spec/root_spec.cr | 2 +- src/placeos-rest-api/controllers/metadata.cr | 4 ++-- src/placeos-rest-api/controllers/root.cr | 2 +- src/placeos-rest-api/controllers/systems.cr | 4 ++-- src/placeos-rest-api/utilities/current-user.cr | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/shard.lock b/shard.lock index f16fbfc9..5745e3b5 100644 --- a/shard.lock +++ b/shard.lock @@ -151,11 +151,11 @@ shards: placeos-build: git: https://github.com/placeos/build.git - version: 0.4.0 + version: 0.5.2+git.commit.b2a8e3876ab00648a4dc2f6fa63e59427d535f21 placeos-compiler: git: https://github.com/placeos/compiler.git - version: 4.4.1 + version: 4.4.2 placeos-core: git: https://github.com/placeos/core.git @@ -175,7 +175,7 @@ shards: placeos-models: git: https://github.com/placeos/models.git - version: 5.8.0 + version: 5.9.1 placeos-resource: git: https://github.com/place-labs/resource.git @@ -211,7 +211,7 @@ shards: rethinkdb: git: https://github.com/kingsleyh/crystal-rethinkdb.git - version: 0.2.3 + version: 0.3.0 rethinkdb-orm: git: https://github.com/spider-gazelle/rethinkdb-orm.git diff --git a/shard.yml b/shard.yml index 958c35b8..c9eccda5 100644 --- a/shard.yml +++ b/shard.yml @@ -45,7 +45,7 @@ dependencies: # For build client placeos-build: github: placeos/build - version: ~> 0.4 + branch: main # For core client placeos-core: diff --git a/spec/helper.cr b/spec/helper.cr index 7cc09695..b61aab9f 100644 --- a/spec/helper.cr +++ b/spec/helper.cr @@ -51,7 +51,7 @@ end CREATION_LOCK = Mutex.new(protection: :reentrant) # Yield an authenticated user, and a header with X-API-Key set -def x_api_authentication(sys_admin : Bool = true, support : Bool = true, scope = ["public"] of String) +def x_api_authentication(sys_admin : Bool = true, support : Bool = true, scope = [PlaceOS::Model::UserJWT::PUBLIC]) CREATION_LOCK.synchronize do user, _header = authentication(sys_admin, support, scope) unless api_key = user.api_tokens.first? @@ -71,7 +71,7 @@ end # Yield an authenticated user, and a header with Authorization bearer set # This method is synchronised due to the redundant top-level calls. -def authentication(sys_admin : Bool = true, support : Bool = true, scope = ["public"] of String) +def authentication(sys_admin : Bool = true, support : Bool = true, scope = [PlaceOS::Model::UserJWT::PUBLIC]) CREATION_LOCK.synchronize do test_user_email = "test-admin-#{sys_admin ? "1" : "0"}-supp-#{support ? "1" : "0"}-rest-api@place.tech" existing = PlaceOS::Model::User.find_all([test_user_email], index: :email).first? diff --git a/spec/root_spec.cr b/spec/root_spec.cr index c5236df4..57e32589 100644 --- a/spec/root_spec.cr +++ b/spec/root_spec.cr @@ -101,7 +101,7 @@ module PlaceOS::Api end context "guests" do - _, guest_header = authentication(sys_admin: false, support: false, scope: ["guest"]) + _, guest_header = authentication(sys_admin: false, support: false, scope: [PlaceOS::Model::UserJWT::GUEST]) it "prevented access to non-guest channels " do result = curl("POST", File.join(base, "signal?channel=dummy"), body: "hello", headers: guest_header) diff --git a/src/placeos-rest-api/controllers/metadata.cr b/src/placeos-rest-api/controllers/metadata.cr index 9fbc675a..35f43885 100644 --- a/src/placeos-rest-api/controllers/metadata.cr +++ b/src/placeos-rest-api/controllers/metadata.cr @@ -26,7 +26,7 @@ module PlaceOS::Api name = params["name"]?.presence # Guest JWTs include the control system id that they have access to - if user_token.scope.includes?("guest") + if user_token.guest_scope? head :forbidden unless name && guest_ids.includes?(parent_id) end @@ -51,7 +51,7 @@ module PlaceOS::Api include_parent = params.has_key?("include_parent") ? params["include_parent"].downcase == "true" : true # Guest JWTs include the control system id that they have access to - if user_token.scope.includes?("guest") + if user_token.guest_scope? head :forbidden unless name && guest_ids.includes?(parent_id) end diff --git a/src/placeos-rest-api/controllers/root.cr b/src/placeos-rest-api/controllers/root.cr index e63646ea..0b73dc5d 100644 --- a/src/placeos-rest-api/controllers/root.cr +++ b/src/placeos-rest-api/controllers/root.cr @@ -148,7 +148,7 @@ module PlaceOS::Api args = SignalParams.new(params).validate! channel = args.channel - if user_token.scope.includes?("guest") + if user_token.guest_scope? head :forbidden unless channel.includes?("/guest/") end diff --git a/src/placeos-rest-api/controllers/systems.cr b/src/placeos-rest-api/controllers/systems.cr index 124389dc..9b4ace99 100644 --- a/src/placeos-rest-api/controllers/systems.cr +++ b/src/placeos-rest-api/controllers/systems.cr @@ -135,7 +135,7 @@ module PlaceOS::Api # Renders a control system def show # Guest JWTs include the control system id that they have access to - if user_token.scope.includes?("guest") + if user_token.guest_scope? head :forbidden unless user_token.user.roles.includes?(current_control_system.id) render json: current_control_system end @@ -187,7 +187,7 @@ module PlaceOS::Api # get "/:sys_id/zones", :sys_zones do # Guest JWTs include the control system id that they have access to - if user_token.scope.includes?("guest") + if user_token.guest_scope? head :forbidden unless user_token.user.roles.includes?(params["sys_id"]) end diff --git a/src/placeos-rest-api/utilities/current-user.cr b/src/placeos-rest-api/utilities/current-user.cr index 24ca275b..e3d84802 100644 --- a/src/placeos-rest-api/utilities/current-user.cr +++ b/src/placeos-rest-api/utilities/current-user.cr @@ -60,7 +60,7 @@ module PlaceOS::Api def check_oauth_scope utoken = user_token - unless utoken.scope.includes?("public") + unless utoken.public_scope? Log.warn { {message: "unknown scope #{utoken.scope}", action: "authorize!", host: request.hostname, id: utoken.id} } raise Error::Unauthorized.new "public scope required for access" end From fa64b05119b89ad0d141e370e62c8167ace34568 Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Tue, 17 Aug 2021 20:11:37 +1000 Subject: [PATCH 09/29] fix(api:repositories): commit listing --- spec/helper.cr | 30 +++++++ spec/repositories_spec.cr | 90 +++++++++---------- .../controllers/repositories.cr | 6 +- 3 files changed, 77 insertions(+), 49 deletions(-) diff --git a/spec/helper.cr b/spec/helper.cr index b61aab9f..a1107dbd 100644 --- a/spec/helper.cr +++ b/spec/helper.cr @@ -225,3 +225,33 @@ macro test_crd(klass, controller_klass) {{ klass.id }}.find(id).should be_nil end end + +class MockServer + include ActionController::Router + + def initialize + init_routes + end + + private def init_routes + {% for klass in ActionController::Base::CONCRETE_CONTROLLERS %} + {{klass}}.__init_routes__(self) + {% end %} + end +end + +MOCK_SERVER = MockServer.new + +abstract class PlaceOS::Api::Application < ActionController::Base + def self.with_request(verb, path, expect_failure = false) + io = IO::Memory.new + context = context(verb.upcase, path) + MOCK_SERVER.route_handler.search_route(context) + context.response.output = io + + yield new(context) + io.rewind + context.response.status.success?.should (expect_failure ? be_false : be_true) + context + end +end diff --git a/spec/repositories_spec.cr b/spec/repositories_spec.cr index 12c61aee..d44f19c0 100644 --- a/spec/repositories_spec.cr +++ b/spec/repositories_spec.cr @@ -69,53 +69,14 @@ module PlaceOS::Api end end - describe "GET /:id/commits" do - context "interface" do - pending "fetches the commits for a repository" do - end - end - - context "driver" do - repo = Model::Generator.repository(type: :driver) - repo.uri = "https://github.com/placeOS/private-drivers" + describe "driver only actions" do + repo = Model::Generator.repository(type: :interface) + before_all do repo.save! - - it "fetches the commits for a repository" do - id = repo.id.as(String) - path = "#{base}#{id}/commits" - result = curl( - method: "GET", - path: path, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) - - result.status.should eq HTTP::Status::OK - Array(String).from_json(result.body).should_not be_empty - end - - it "fetches the commits for a file" do - id = repo.id.as(String) - params = HTTP::Params{ - "driver" => "drivers/place/private_helper.cr", - } - path = "#{base}#{id}/commits?#{params}" - result = curl( - method: "GET", - path: path, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) - - result.status.should eq HTTP::Status::OK - Array(String).from_json(result.body).should_not be_empty - end end - end - describe "driver only actions" do it "errors if enumerating drivers in an interface repo" do - repository = Model::Generator.repository(type: :interface).save! - - id = repository.id.as(String) + id = repo.id.as(String) path = "#{base}#{id}/drivers" result = curl( method: "GET", @@ -127,9 +88,7 @@ module PlaceOS::Api end it "errors when requesting driver details from an interface repo" do - repository = Model::Generator.repository(type: Model::Repository::Type::Interface).save! - - id = repository.id.as(String) + id = repo.id.as(String) path = "#{base}#{id}/details" result = curl( method: "GET", @@ -141,6 +100,45 @@ module PlaceOS::Api end end end + + describe "GET /:id/commits" do + context "interface" do + pending "fetches the commits for a repository" do + end + end + + context "driver" do + repo = Model::Generator.repository(type: :driver).tap do |r| + r.uri = "https://github.com/placeOS/private-drivers" + end + + before_all do + repo.save! + end + + it "fetches the commits for a repository" do + id = repo.id.as(String) + response = Api::Repositories + .with_request("GET", "#{base}#{id}/commits", &.commits) + .response + response.status.should eq HTTP::Status::OK + Array(String).from_json(response.output).should_not be_empty + end + + it "fetches the commits for a file" do + id = repo.id.as(String) + params = HTTP::Params{ + "driver" => "drivers/place/private_helper.cr", + } + response = Api::Repositories + .with_request("GET", "#{base}#{id}/commits?#{params}", &.commits) + .response + + response.status.should eq HTTP::Status::OK + Array(String).from_json(response.output).should_not be_empty + end + end + end end end end diff --git a/src/placeos-rest-api/controllers/repositories.cr b/src/placeos-rest-api/controllers/repositories.cr index 6968cb31..746c053e 100644 --- a/src/placeos-rest-api/controllers/repositories.cr +++ b/src/placeos-rest-api/controllers/repositories.cr @@ -123,7 +123,7 @@ module PlaceOS::Api render json: commits end - def self.commits(repository : Model::Repository, request_id : String, file_name : String? = nil, branch : String? = nil, limit : Int32? = nil) + def self.commits(repository : Model::Repository, request_id : String, file_name : String? = nil, branch : String? = nil, limit : Int32? = nil) : Array(String) limit = 50 if limit.nil? branch = "master" if branch.nil? @@ -136,12 +136,12 @@ module PlaceOS::Api else client.repository_commits(**args) end - end + end.map(&.commit) in .interface? # Dial the frontends service Frontends::Client.client(request_id: request_id) do |frontends_client| frontends_client.commits(repository.folder_name, limit) - end + end.map(&.[:commit]) end end From 2364acece2fbca61795d1304e7d46ce632ce13bc Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Fri, 20 Aug 2021 14:13:01 +1000 Subject: [PATCH 10/29] ci: add dep on build --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 774208c5..806172a4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,6 +41,7 @@ services: - ./spec:/app/spec - ./src:/app/src depends_on: + - build - elastic - etcd - redis From 7fefd87600ad9fde9f8e0a129e556614e1b5bf0d Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Thu, 2 Jun 2022 12:33:16 +1000 Subject: [PATCH 11/29] refactor: use updated core client --- src/placeos-rest-api/controllers/repositories.cr | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/placeos-rest-api/controllers/repositories.cr b/src/placeos-rest-api/controllers/repositories.cr index 07f20449..6c67a3b4 100644 --- a/src/placeos-rest-api/controllers/repositories.cr +++ b/src/placeos-rest-api/controllers/repositories.cr @@ -129,8 +129,7 @@ module PlaceOS::Api get "/:id/drivers", :drivers do drivers = Build::Client.client &.discover_drivers( url: current_repository.uri, - commit: current_repository.commit_hash, - branch: current_repository.branch, + ref: current_repository.commit_hash || current_repository.branch, username: current_repository.username, password: current_repository.decrypt_password, request_id: request_id, From ddd103dfc02919801b81a1fa66ebffedc8eb23c6 Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Thu, 2 Jun 2022 12:34:56 +1000 Subject: [PATCH 12/29] chore: remove todo in docker-compose --- docker-compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5ab48796..d993da80 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -61,7 +61,6 @@ services: <<: *redis-client-env <<: *rethinkdb-client-env - # TODO: Mock and remove after interface has stabilised build: image: placeos/build:${PLACE_BUILD_TAG:-nightly} restart: always From 740ed4c591241df43465df743b7784b62453dcfe Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Thu, 2 Jun 2022 15:21:31 +1000 Subject: [PATCH 13/29] ci: specify build branch of core --- docker-compose.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index d993da80..b5221ed1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -130,7 +130,8 @@ services: PLACE_URI: https://${PLACE_DOMAIN:-localhost:8443} core: # Module coordinator - image: placeos/core:${PLACE_CORE_TAG:-nightly} + # image: placeos/core:${PLACE_CORE_TAG:-nightly} + image: ghcr.io/placeos/core:feat-build restart: always hostname: core depends_on: From 56ec9966e1667044773a434152adadd729652ac2 Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Thu, 2 Jun 2022 16:35:31 +1000 Subject: [PATCH 14/29] ci: add GHCR_PAT to workflow --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33d5193a..81bdbe30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ jobs: with: todo_issues: true first_commit: 0a1e7680dc203f278f18fbe1f81bfd2713b83d1c + CR_PAT: ${{ secrets.GHCR_PAT }} crystal-style: uses: PlaceOS/.github/.github/workflows/crystal-style.yml@main From 12137c0a6b626392e783bd55c61cb4c0c44ec0b7 Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Thu, 2 Jun 2022 16:40:34 +1000 Subject: [PATCH 15/29] ci: wtf gha --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81bdbe30..527f91ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,13 +4,16 @@ on: paths-ignore: - "*.md" +env: + CR_PAT: ${{ secrets.GHCR_PAT }} + jobs: test: uses: PlaceOS/.github/.github/workflows/containerised-test.yml@main with: todo_issues: true first_commit: 0a1e7680dc203f278f18fbe1f81bfd2713b83d1c - CR_PAT: ${{ secrets.GHCR_PAT }} + CR_PAT: ${{ env.CR_PAT }} crystal-style: uses: PlaceOS/.github/.github/workflows/crystal-style.yml@main From 2648ac839317df81c4211eb7cec44b5a9724ba64 Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Thu, 2 Jun 2022 16:45:06 +1000 Subject: [PATCH 16/29] ci: use secrets input --- .github/workflows/ci.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 527f91ed..1f20da15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,16 +4,14 @@ on: paths-ignore: - "*.md" -env: - CR_PAT: ${{ secrets.GHCR_PAT }} - jobs: test: uses: PlaceOS/.github/.github/workflows/containerised-test.yml@main with: todo_issues: true first_commit: 0a1e7680dc203f278f18fbe1f81bfd2713b83d1c - CR_PAT: ${{ env.CR_PAT }} + secrets: + CR_PAT: ${{ secrets.GHCR_PAT }} crystal-style: uses: PlaceOS/.github/.github/workflows/crystal-style.yml@main From 015480a6c271b752eda34bf8e0cadc9eb308f1bf Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Thu, 2 Jun 2022 17:00:28 +1000 Subject: [PATCH 17/29] fix: update action-controller handler args --- spec/controllers/repositories_spec.cr | 4 ++-- spec/helper.cr | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/spec/controllers/repositories_spec.cr b/spec/controllers/repositories_spec.cr index c5d3cac4..df0ba57f 100644 --- a/spec/controllers/repositories_spec.cr +++ b/spec/controllers/repositories_spec.cr @@ -118,7 +118,7 @@ module PlaceOS::Api it "fetches the commits for a repository" do id = repo.id.as(String) response = Api::Repositories - .with_request("GET", "#{base}#{id}/commits", &.commits) + .with_request("GET", "#{base}#{id}/commits", route_params: {"id" => id}, &.commits) .response response.status.should eq HTTP::Status::OK Array(String).from_json(response.output).should_not be_empty @@ -130,7 +130,7 @@ module PlaceOS::Api "driver" => "drivers/place/private_helper.cr", } response = Api::Repositories - .with_request("GET", "#{base}#{id}/commits?#{params}", &.commits) + .with_request("GET", "#{base}#{id}/commits?#{params}", route_params: {"id" => id}, &.commits) .response response.status.should eq HTTP::Status::OK diff --git a/spec/helper.cr b/spec/helper.cr index 2aa3095f..c2b3dce4 100644 --- a/spec/helper.cr +++ b/spec/helper.cr @@ -425,14 +425,17 @@ end MOCK_SERVER = MockServer.new abstract class PlaceOS::Api::Application < ActionController::Base - def self.with_request(verb, path, expect_failure = false) + def self.with_request(verb, path, expect_failure = false, route_params = nil) io = IO::Memory.new context = context(verb.upcase, path) - MOCK_SERVER.route_handler.search_route(context) + + context.route_params = route_params if route_params + + MOCK_SERVER.route_handler.search_route(verb, path, "#{verb.downcase}#{path}", context) context.response.output = io yield new(context) - io.rewind + context.response.status.success?.should(expect_failure ? be_false : be_true) context end From 43f58888d39eaa87d42fce9b0e3a503a3ed3387d Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Wed, 8 Jun 2022 16:59:47 +1000 Subject: [PATCH 18/29] refactor: action-controller 5 (really not passing atm) --- docker-compose.yml | 1 - shard.lock | 22 +- shard.override.yml | 6 +- shard.yml | 4 +- spec/controllers/api_key_spec.cr | 22 +- spec/controllers/asset_instances_spec.cr | 140 ++-- spec/controllers/asset_spec.cr | 118 ++- spec/controllers/brokers_spec.cr | 62 +- spec/controllers/drivers_spec.cr | 182 +++-- spec/controllers/edges_spec.cr | 93 ++- spec/controllers/metadata_spec.cr | 595 ++++++++------- spec/controllers/modules_spec.cr | 361 +++++---- spec/controllers/mqtt_spec.cr | 68 +- spec/controllers/repositories_spec.cr | 216 +++--- spec/controllers/root_spec.cr | 159 ++-- spec/controllers/settings_spec.cr | 345 +++++---- spec/controllers/system-triggers_spec.cr | 207 +++--- spec/controllers/systems_spec.cr | 889 +++++++++++------------ spec/controllers/triggers_spec.cr | 112 ++- spec/controllers/users_spec.cr | 260 ++++--- spec/controllers/zones_spec.cr | 95 ++- spec/helper.cr | 307 +------- spec/scope_helper.cr | 43 -- spec/spec_constants.cr | 8 - spec/spec_helpers/client.cr | 8 + spec/{ => spec_helpers}/core_helper.cr | 0 spec/{ => spec_helpers}/http_mocks.cr | 0 spec/spec_helpers/scopes.cr | 42 ++ spec/spec_helpers/specs.cr | 242 ++++++ spec/websocket/session_spec.cr | 284 ++++---- test | 2 - 31 files changed, 2351 insertions(+), 2542 deletions(-) delete mode 100644 spec/scope_helper.cr delete mode 100644 spec/spec_constants.cr create mode 100644 spec/spec_helpers/client.cr rename spec/{ => spec_helpers}/core_helper.cr (100%) rename spec/{ => spec_helpers}/http_mocks.cr (100%) create mode 100644 spec/spec_helpers/scopes.cr create mode 100644 spec/spec_helpers/specs.cr diff --git a/docker-compose.yml b/docker-compose.yml index b5221ed1..3199acdb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -130,7 +130,6 @@ services: PLACE_URI: https://${PLACE_DOMAIN:-localhost:8443} core: # Module coordinator - # image: placeos/core:${PLACE_CORE_TAG:-nightly} image: ghcr.io/placeos/core:feat-build restart: always hostname: core diff --git a/shard.lock b/shard.lock index 51bd7d77..f0aaddc8 100644 --- a/shard.lock +++ b/shard.lock @@ -5,9 +5,9 @@ shards: git: https://github.com/place-labs/crystalemail.git version: 0.2.6 - action-controller: + action-controller: # Overridden git: https://github.com/spider-gazelle/action-controller.git - version: 4.10.1 + version: 5.1.4 active-model: git: https://github.com/spider-gazelle/active-model.git @@ -121,6 +121,10 @@ shards: git: https://github.com/crystal-community/hardware.git version: 0.5.2 + hot_topic: + git: https://github.com/jgaskins/hot_topic.git + version: 0.1.0+git.commit.c4577d949221d535f29162343bf503b578308954 + hound-dog: git: https://github.com/place-labs/hound-dog.git version: 2.9.0 @@ -163,7 +167,7 @@ shards: neuroplastic: git: https://github.com/spider-gazelle/neuroplastic.git - version: 1.11.0 + version: 1.11.1 open_api: git: https://github.com/elbywan/open_api.cr.git @@ -179,11 +183,11 @@ shards: opentelemetry-api: git: https://github.com/wyhaines/opentelemetry-api.cr.git - version: 0.3.1 + version: 0.3.4 opentelemetry-instrumentation: git: https://github.com/wyhaines/opentelemetry-instrumentation.cr.git - version: 0.3.2+git.commit.47a7b5c243edf08dd9f7a4e2270a7698489dcb1b + version: 0.3.6+git.commit.64fa1db57f535c511e6515620e7f8e4ce8a72780 pars: # Overridden git: https://github.com/spider-gazelle/pars.git @@ -199,7 +203,7 @@ shards: placeos-build: git: https://github.com/placeos/build.git - version: 1.0.3+git.commit.2311332c276689476fc634fc4837cedd1ac9d73a + version: 1.0.3+git.commit.67e7e5aebe872b8d7369fa572e55e2be26d94503 placeos-compiler: git: https://github.com/placeos/compiler.git @@ -207,11 +211,11 @@ shards: placeos-core: git: https://github.com/placeos/core.git - version: 4.3.1+git.commit.ce5cbf752e0e6da73b6009bd1b3c53d5d0d05aee + version: 4.3.1+git.commit.6995c2ae32c74b9a44d7875ff2f55abd5603bbad placeos-core-client: # Overridden git: https://github.com/placeos/core-client.git - version: 0.5.2+git.commit.26adb20be800f052fbbb14182b6548fb26589f87 + version: 1.0.0 placeos-driver: git: https://github.com/placeos/driver.git @@ -223,7 +227,7 @@ shards: placeos-log-backend: git: https://github.com/place-labs/log-backend.git - version: 0.10.2 + version: 0.10.3 placeos-models: git: https://github.com/placeos/models.git diff --git a/shard.override.yml b/shard.override.yml index 3942fbc6..3f4daca0 100644 --- a/shard.override.yml +++ b/shard.override.yml @@ -7,4 +7,8 @@ dependencies: placeos-core-client: github: placeos/core-client - branch: feat/build + version: ~> 1.0 + + action-controller: + github: spider-gazelle/action-controller + version: ~> 5.1 diff --git a/shard.yml b/shard.yml index debf6fdb..a0051d6d 100644 --- a/shard.yml +++ b/shard.yml @@ -10,7 +10,7 @@ dependencies: # Server framework action-controller: github: spider-gazelle/action-controller - version: ~> 4.1 + version: ~> 5.1 # Data validation library active-model: @@ -49,7 +49,7 @@ dependencies: # For core client placeos-core-client: github: placeos/core-client - version: ~> 0.3 + version: ~> 1.0 # For driver state helpers placeos-driver: diff --git a/spec/controllers/api_key_spec.cr b/spec/controllers/api_key_spec.cr index 26c67d1d..3e644f3a 100644 --- a/spec/controllers/api_key_spec.cr +++ b/spec/controllers/api_key_spec.cr @@ -1,25 +1,21 @@ require "../helper" -require "../scope_helper" module PlaceOS::Api describe ApiKeys do _, scoped_authorization_header = x_api_authentication - base = ApiKeys::NAMESPACE[0] - with_server do - Specs.test_404(base, model_name: Model::ApiKey.table_name, headers: scoped_authorization_header) + Specs.test_404(ApiKeys.base_route, model_name: Model::ApiKey.table_name, headers: scoped_authorization_header) - describe "index", tags: "search" do - Specs.test_base_index(Model::ApiKey, ApiKeys) - end + describe "index", tags: "search" do + Specs.test_base_index(Model::ApiKey, ApiKeys) + end - describe "CRUD operations", tags: "crud" do - Specs.test_crd(Model::ApiKey, ApiKeys) - end + describe "CRUD operations", tags: "crud" do + Specs.test_crd(Model::ApiKey, ApiKeys) + end - describe "scopes" do - Specs.test_controller_scope(ApiKeys) - end + describe "scopes" do + Specs.test_controller_scope(ApiKeys) end end end diff --git a/spec/controllers/asset_instances_spec.cr b/spec/controllers/asset_instances_spec.cr index ea2020e9..0d32a906 100644 --- a/spec/controllers/asset_instances_spec.cr +++ b/spec/controllers/asset_instances_spec.cr @@ -4,83 +4,77 @@ require "timecop" module PlaceOS::Api describe AssetInstances do _, authorization_header = authentication - base = AssetInstances::NAMESPACE[0] - with_server do - Specs.test_404( - base, - model_name: Model::AssetInstance.table_name, - headers: authorization_header, - ) + Specs.test_404( + AssetInstances.base_route, + model_name: Model::AssetInstance.table_name, + headers: authorization_header, + ) - describe "index", tags: "search" do - Specs.test_base_index(klass: Model::AssetInstance, controller_klass: AssetInstances) + describe "index", tags: "search" do + Specs.test_base_index(klass: Model::AssetInstance, controller_klass: AssetInstances) + end + + describe "CRUD operations", tags: "crud" do + it "create" do + asset_instance = Model::Generator.asset_instance.save! + body = asset_instance.to_json + + result = client.post( + path: AssetInstances.base_route, + body: body, + headers: authorization_header, + ) + + result.status_code.should eq 201 + body = result.body.not_nil! + Model::AssetInstance.find(JSON.parse(body)["id"].as_s).try &.destroy end - describe "CRUD operations", tags: "crud" do - it "create" do - asset_instance = Model::Generator.asset_instance.save! - body = asset_instance.to_json - - result = curl( - method: "POST", - path: base, - body: body, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) - - result.status_code.should eq 201 - body = result.body.not_nil! - Model::AssetInstance.find(JSON.parse(body)["id"].as_s).try &.destroy - end - - it "show" do - asset_instance = Model::Generator.asset_instance.save! - path = base + asset_instance.id.not_nil! - - result = curl( - method: "GET", - path: path, - headers: authorization_header, - ) - - fetched = Model::AssetInstance.from_trusted_json(result.body) - fetched.id.should eq asset_instance.id - end - - it "update" do - asset_instance = Model::Generator.asset_instance.save! - - id = asset_instance.id.not_nil! - path = File.join(base, id) - - result = curl( - method: "PATCH", - path: path, - body: {approval: true}.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) - - result.status_code.should eq 200 - updated = Model::AssetInstance.from_trusted_json(result.body) - - updated.id.should eq id - updated.approval.should be_true - updated.destroy - end - - it "destroy" do - model = PlaceOS::Model::Generator.asset_instance.save! - model.persisted?.should be_true - - id = model.id.not_nil! - path = File.join(base, id) - - result = curl(method: "DELETE", path: path, headers: authorization_header) - result.status_code.should eq 200 - - Model::AssetInstance.find(id.as(String)).should be_nil - end + it "show" do + asset_instance = Model::Generator.asset_instance.save! + path = AssetInstances.base_route + asset_instance.id.not_nil! + + result = client.get( + path: path, + headers: authorization_header, + ) + + fetched = Model::AssetInstance.from_trusted_json(result.body) + fetched.id.should eq asset_instance.id + end + + it "update" do + asset_instance = Model::Generator.asset_instance.save! + + id = asset_instance.id.not_nil! + path = File.join(AssetInstances.base_route, id) + + result = client.patch( + path: path, + body: {approval: true}.to_json, + headers: authorization_header, + ) + + result.status_code.should eq 200 + updated = Model::AssetInstance.from_trusted_json(result.body) + + updated.id.should eq id + updated.approval.should be_true + updated.destroy + end + + it "destroy" do + model = PlaceOS::Model::Generator.asset_instance.save! + model.persisted?.should be_true + + id = model.id.not_nil! + path = File.join(AssetInstances.base_route, id) + + result = client.delete(path: path, headers: authorization_header) + result.status_code.should eq 200 + + Model::AssetInstance.find(id.as(String)).should be_nil end end end diff --git a/spec/controllers/asset_spec.cr b/spec/controllers/asset_spec.cr index 21fd5a83..7b398a6a 100644 --- a/spec/controllers/asset_spec.cr +++ b/spec/controllers/asset_spec.cr @@ -3,84 +3,78 @@ require "../helper" module PlaceOS::Api describe Assets do _, authorization_header = authentication - base = Assets::NAMESPACE[0] - with_server do - Specs.test_404(base, model_name: Model::Asset.table_name, headers: authorization_header) + Specs.test_404(Assets.base_route, model_name: Model::Asset.table_name, headers: authorization_header) - describe "index", tags: "search" do - Specs.test_base_index(klass: Model::Asset, controller_klass: Assets) - end + describe "index", tags: "search" do + Specs.test_base_index(klass: Model::Asset, controller_klass: Assets) + end - describe "GET /asset-instances/:id/instances" do - it "lists instances for an Asset" do - asset = Model::Generator.asset.save! - instances = Array(Model::AssetInstance).new(size: 3) { Model::Generator.asset_instance(asset).save! } - - response = curl( - method: "GET", - path: File.join(base, asset.id.not_nil!, "asset_instances"), - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) - - # Can't use from_json directly on the model as `id` will not be parsed - result = Array(JSON::Any).from_json(response.body).map { |d| Model::AssetInstance.from_trusted_json(d.to_json) } - result.all? { |i| i.asset_id == asset.id }.should be_true - instances.compact_map(&.id).sort!.should eq result.compact_map(&.id).sort! - end - end + describe "GET /asset-instances/:id/instances" do + it "lists instances for an Asset" do + asset = Model::Generator.asset.save! + instances = Array(Model::AssetInstance).new(size: 3) { Model::Generator.asset_instance(asset).save! } + + response = client.get( + path: File.join(Assets.base_route, asset.id.not_nil!, "asset_instances"), + headers: authorization_header, + ) - describe "CRUD operations", tags: "crud" do - Specs.test_crd(klass: Model::Asset, controller_klass: Assets) + # Can't use from_json directly on the model as `id` will not be parsed + result = Array(JSON::Any).from_json(response.body).map { |d| Model::AssetInstance.from_trusted_json(d.to_json) } + result.all? { |i| i.asset_id == asset.id }.should be_true + instances.compact_map(&.id).sort!.should eq result.compact_map(&.id).sort! end + end - it "update" do - asset = Model::Generator.asset.save! - original_name = asset.name + describe "CRUD operations", tags: "crud" do + Specs.test_crd(klass: Model::Asset, controller_klass: Assets) + end - asset.name = UUID.random.to_s + it "update" do + asset = Model::Generator.asset.save! + original_name = asset.name - id = asset.id.as(String) - path = File.join(base, id) - result = curl( - method: "PATCH", - path: path, - body: asset.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) + asset.name = UUID.random.to_s - result.status_code.should eq 200 - updated = Model::Asset.from_trusted_json(result.body) + id = asset.id.as(String) + path = File.join(Assets.base_route, id) + result = client.patch( + path: path, + body: asset.to_json, + headers: authorization_header, + ) - updated.id.should eq asset.id - updated.name.should_not eq original_name - updated.destroy - end + result.status_code.should eq 200 + updated = Model::Asset.from_trusted_json(result.body) - describe "show" do - it "includes asset_instances with truthy `instances`" do - asset = Model::Generator.asset.save! - asset_instance = Model::Generator.asset_instance(asset).save! - asset_instance_id = asset_instance.id.as(String) + updated.id.should eq asset.id + updated.name.should_not eq original_name + updated.destroy + end - params = HTTP::Params{"instances" => "true"} - path = "#{base}#{asset.id}?#{params}" + describe "show" do + it "includes asset_instances with truthy `instances`" do + asset = Model::Generator.asset.save! + asset_instance = Model::Generator.asset_instance(asset).save! + asset_instance_id = asset_instance.id.as(String) - result = curl( - method: "GET", - path: path, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) + params = HTTP::Params{"instances" => "true"} + path = "#{Assets.base}#{asset.id}?#{params}" - response = JSON.parse(result.body) - response["asset_instances"].as_a?.try &.first?.try &.["id"].to_s.should eq asset_instance_id - end + result = client.get( + path: path, + headers: authorization_header, + ) + + response = JSON.parse(result.body) + response["asset_instances"].as_a?.try &.first?.try &.["id"].to_s.should eq asset_instance_id end end + end - describe "scopes" do - Specs.test_controller_scope(Assets) - Specs.test_update_write_scope(Assets) - end + describe "scopes" do + Specs.test_controller_scope(Assets) + Specs.test_update_write_scope(Assets) end end diff --git a/spec/controllers/brokers_spec.cr b/spec/controllers/brokers_spec.cr index 3f141f0d..934cade3 100644 --- a/spec/controllers/brokers_spec.cr +++ b/spec/controllers/brokers_spec.cr @@ -3,44 +3,40 @@ require "../helper" module PlaceOS::Api describe Brokers do _, authorization_header = authentication - base = Brokers::NAMESPACE[0] - with_server do - Specs.test_404(base, model_name: Model::Broker.table_name, headers: authorization_header) + Specs.test_404(Brokers.base_route, model_name: Model::Broker.table_name, headers: authorization_header) - describe "index", tags: "search" do - Specs.test_base_index(klass: Model::Broker, controller_klass: Brokers) - end + describe "index", tags: "search" do + Specs.test_base_index(klass: Model::Broker, controller_klass: Brokers) + end - describe "CRUD operations", tags: "crud" do - Specs.test_crd(Model::Broker, Brokers) - - it "update" do - broker = Model::Generator.broker.save! - original_name = broker.name - broker.name = random_name - - id = broker.id.as(String) - path = File.join(base, id) - result = curl( - method: "PATCH", - path: path, - body: broker.changed_attributes.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) - - result.status_code.should eq 200 - updated = Model::Broker.from_trusted_json(result.body) - - updated.id.should eq broker.id - updated.name.should_not eq original_name - end - end + describe "CRUD operations", tags: "crud" do + Specs.test_crd(Model::Broker, Brokers) + + it "update" do + broker = Model::Generator.broker.save! + original_name = broker.name + broker.name = random_name - describe "scopes" do - Specs.test_update_write_scope(Brokers) - Specs.test_controller_scope(Brokers) + id = broker.id.as(String) + path = File.join(Brokers.base_route, id) + result = client.patch( + path: path, + body: broker.changed_attributes.to_json, + headers: authorization_header, + ) + + result.status_code.should eq 200 + updated = Model::Broker.from_trusted_json(result.body) + + updated.id.should eq broker.id + updated.name.should_not eq original_name end end + + describe "scopes" do + Specs.test_update_write_scope(Brokers) + Specs.test_controller_scope(Brokers) + end end end diff --git a/spec/controllers/drivers_spec.cr b/spec/controllers/drivers_spec.cr index fd925b81..4c15b7a5 100644 --- a/spec/controllers/drivers_spec.cr +++ b/spec/controllers/drivers_spec.cr @@ -1,122 +1,114 @@ require "../helper" -require "../core_helper" module PlaceOS::Api describe Drivers do _, authorization_header = authentication - base = Drivers::NAMESPACE[0] - - with_server do - describe "index", tags: "search" do - Specs.test_base_index(klass: Model::Driver, controller_klass: Drivers) - - it "filters queries by driver role" do - service = Model::Generator.driver(role: Model::Driver::Role::Service) - service.name = random_name - service.save! - - params = HTTP::Params.encode({ - "role" => Model::Driver::Role::Service.to_i.to_s, - "q" => service.name, - }) - - refresh_elastic(Model::Driver.table_name) - path = "#{base}?#{params}" - found = until_expected("GET", path, authorization_header) do |response| - results = Array(Hash(String, JSON::Any)).from_json(response.body) - all_service_roles = results.all? { |r| r["role"] == Model::Driver::Role::Service.to_i } - contains_search_term = results.any? { |r| r["id"] == service.id } - !results.empty? && all_service_roles && contains_search_term - end - - found.should be_true + + describe "index", tags: "search" do + Specs.test_base_index(klass: Model::Driver, controller_klass: Drivers) + + it "filters queries by driver role" do + service = Model::Generator.driver(role: Model::Driver::Role::Service) + service.name = random_name + service.save! + + params = HTTP::Params.encode({ + "role" => Model::Driver::Role::Service.to_i.to_s, + "q" => service.name, + }) + + refresh_elastic(Model::Driver.table_name) + path = "#{Drivers.base_route}?#{params}" + found = until_expected("GET", path, authorization_header) do |response| + results = Array(Hash(String, JSON::Any)).from_json(response.body) + all_service_roles = results.all? { |r| r["role"] == Model::Driver::Role::Service.to_i } + contains_search_term = results.any? { |r| r["id"] == service.id } + !results.empty? && all_service_roles && contains_search_term end + + found.should be_true end + end - Specs.test_404(base, model_name: Model::Driver.table_name, headers: authorization_header) + Specs.test_404(Drivers.base_route, model_name: Model::Driver.table_name, headers: authorization_header) - describe "CRUD operations", tags: "crud" do - before_each do - HttpMocks.reset - end + describe "CRUD operations", tags: "crud" do + before_each do + HttpMocks.reset + end - Specs.test_crd(klass: Model::Driver, controller_klass: Drivers) - - describe "update" do - it "if role is preserved" do - driver = Model::Generator.driver.save! - original_name = driver.name - driver.name = random_name - - id = driver.id.as(String) - path = File.join(base, id) - result = curl( - method: "PATCH", - path: path, - body: driver.changed_attributes.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) - result.success?.should be_true - - updated = Model::Driver.from_trusted_json(result.body) - updated.id.should eq driver.id - updated.name.should_not eq original_name - end - - it "fails if role differs" do - driver = Model::Generator.driver(role: Model::Driver::Role::SSH).save! - driver.role = Model::Driver::Role::Device - id = driver.id.as(String) - path = File.join(base, id) - result = curl( - method: "PATCH", - path: path, - body: driver.changed_attributes.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) - - result.success?.should_not be_true - result.body.should contain "role must not change" - end - end + Specs.test_crd(klass: Model::Driver, controller_klass: Drivers) - it "GET /:id/compiled" do - driver = get_driver - Utils::Changefeeds.await_model_change(driver, timeout: 90.seconds) do |update| - update.destroyed? || !update.recompile_commit? - end + describe "update" do + it "if role is preserved" do + driver = Model::Generator.driver.save! + original_name = driver.name + driver.name = random_name - response = curl( - method: "GET", - path: "#{base}#{driver.id.not_nil!}/compiled", - headers: authorization_header.merge({"Content-Type" => "application/json"}), + id = driver.id.as(String) + path = File.join(Drivers.base_route, id) + result = client.patch( + path: path, + body: driver.changed_attributes.to_json, + headers: authorization_header, ) + result.success?.should be_true - response.success?.should be_true + updated = Model::Driver.from_trusted_json(result.body) + updated.id.should eq driver.id + updated.name.should_not eq original_name end - it "POST /:id/recompile" do - driver = get_driver - - response = curl( - method: "POST", - path: "#{base}#{driver.id.not_nil!}/recompile", - headers: authorization_header.merge({"Content-Type" => "application/json"}), + it "fails if role differs" do + driver = Model::Generator.driver(role: Model::Driver::Role::SSH).save! + driver.role = Model::Driver::Role::Device + id = driver.id.as(String) + path = File.join(Drivers.base_route, id) + result = client.patch( + path: path, + body: driver.changed_attributes.to_json, + headers: authorization_header, ) - response.success?.should be_true - updated = Model::Driver.from_trusted_json(response.body) - updated.commit.starts_with?("RECOMPILE").should be_false + result.success?.should_not be_true + result.body.should contain "role must not change" end end - describe "scopes" do - before_each do - HttpMocks.core_compiled + pending "GET /:id/compiled" do + driver = get_driver + Utils::Changefeeds.await_model_change(driver, timeout: 90.seconds) do |update| + update.destroyed? || !update.recompile_commit? end - Specs.test_controller_scope(Drivers) + response = client.get( + path: "#{Drivers.base_route}#{driver.id.not_nil!}/compiled", + headers: authorization_header, + ) + + response.success?.should be_true end + + it "POST /:id/recompile" do + driver = get_driver + + response = client.post( + path: "#{Drivers.base_route}#{driver.id.not_nil!}/recompile", + headers: authorization_header, + ) + + response.success?.should be_true + updated = Model::Driver.from_trusted_json(response.body) + updated.commit.starts_with?("RECOMPILE").should be_false + end + end + + describe "scopes" do + before_each do + HttpMocks.core_compiled + end + + Specs.test_controller_scope(Drivers) end end end diff --git a/spec/controllers/edges_spec.cr b/spec/controllers/edges_spec.cr index cec66e63..b4fa1cb5 100644 --- a/spec/controllers/edges_spec.cr +++ b/spec/controllers/edges_spec.cr @@ -4,69 +4,64 @@ require "placeos-core/placeos-edge/client" module PlaceOS::Api describe Edges do authenticated_user, authorization_header = authentication - base = Edges::NAMESPACE[0] + Specs.test_404(Edges.base_route, model_name: Model::Edge.table_name, headers: authorization_header) - with_server do - Specs.test_404(base, model_name: Model::Edge.table_name, headers: authorization_header) + describe "index", tags: "search" do + Specs.test_base_index(Model::Edge, Edges) + end - describe "index", tags: "search" do - Specs.test_base_index(Model::Edge, Edges) - end + describe "/control" do + it "authenticates with an API key from a new edge" do + # Create a new edge to test with as the controller would + edge_name = "Test Edge" + edge_host = "localhost" + edge_port = 6000 - describe "/control" do - it "authenticates with an API key from a new edge" do - # Create a new edge to test with as the controller would - edge_name = "Test Edge" - edge_host = "localhost" - edge_port = 6000 + create_body = Model::Edge::CreateBody.new(name: edge_name, user_id: authenticated_user.id.as(String)) + new_edge = Model::Edge.for_user( + user: authenticated_user, + name: create_body.name, + description: create_body.description + ) - create_body = Model::Edge::CreateBody.new(name: edge_name, user_id: authenticated_user.id.as(String)) - new_edge = Model::Edge.for_user( - user: authenticated_user, - name: create_body.name, - description: create_body.description - ) + # Ensure instance variable initialised and edge saved + new_edge.x_api_key + new_edge.save! - # Ensure instance variable initialised and edge saved - new_edge.x_api_key - new_edge.save! - - uri = URI.new(host: edge_host, port: edge_port, query: "api-key=#{new_edge.x_api_key}") - client = PlaceOS::Edge::Client.new( - uri: uri, - secret: new_edge.x_api_key - ) + uri = URI.new(host: edge_host, port: edge_port, query: "api-key=#{new_edge.x_api_key}") + client = PlaceOS::Edge::Client.new( + uri: uri, + secret: new_edge.x_api_key + ) - client.connect do - client.transport.closed?.should_not be_nil - client.disconnect - end + client.connect do + client.transport.closed?.should_not be_nil + client.disconnect end end + end - describe "CRUD operations", tags: "crud" do - Specs.test_crd(Model::Edge, Edges) + describe "CRUD operations", tags: "crud" do + Specs.test_crd(Model::Edge, Edges) - describe "create" do - it "contains the api token in the response" do - result = curl( - method: "POST", - path: base, - body: { - "description" => "", - "name" => "test-edge", - }.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) + describe "create" do + it "contains the api token in the response" do + result = client.post( + path: Edges.base_route, + body: { + "description" => "", + "name" => "test-edge", + }.to_json, + headers: authorization_header, + ) - JSON.parse(result.body)["x_api_key"]?.try(&.as_s?).should_not be_nil - end + JSON.parse(result.body)["x_api_key"]?.try(&.as_s?).should_not be_nil end end + end - describe "scopes" do - Specs.test_controller_scope(Edges) - end + describe "scopes" do + Specs.test_controller_scope(Edges) end end end diff --git a/spec/controllers/metadata_spec.cr b/spec/controllers/metadata_spec.cr index 7476adcb..5ba32d5d 100644 --- a/spec/controllers/metadata_spec.cr +++ b/spec/controllers/metadata_spec.cr @@ -4,11 +4,238 @@ require "timecop" module PlaceOS::Api describe Metadata do _authenticated_user, authorization_header = authentication - base = Metadata::NAMESPACE[0] - with_server do - describe "GET /metadata/:id/children/" do - it "shows zone children metadata" do + describe "GET /metadata/:id/children/" do + it "shows zone children metadata" do + parent = Model::Generator.zone.save! + parent_id = parent.id.as(String) + + 3.times do + child = Model::Generator.zone + child.parent_id = parent_id + child.save! + Model::Generator.metadata(parent: child.id).save! + end + + result = client.get( + path: "#{Metadata.base_route}/#{parent_id}/children", + headers: authorization_header, + ) + + Array(NamedTuple(zone: JSON::Any, metadata: Hash(String, Model::Metadata::Interface))) + .from_json(result.body) + .tap(&.size.should eq(3)) + .count(&.[:metadata].empty?.!) + .should eq 3 + + parent.destroy + end + + it "filters zone children metadata" do + parent = Model::Generator.zone.save! + parent_id = parent.id.as(String) + + children = Array.new(size: 3) do + child = Model::Generator.zone + child.parent_id = parent_id + child.save! + Model::Generator.metadata(parent: child.id).save! + child + end + + # Create a single special metadata to filter on + Model::Generator.metadata(name: "special", parent: children.first.id).save! + + result = client.get( + path: "#{Metadata.base_route}/#{parent_id}/children?name=special", + headers: authorization_header, + ) + + Array(NamedTuple(zone: JSON::Any, metadata: Hash(String, Model::Metadata::Interface))) + .from_json(result.body) + .count(&.[:metadata].empty?.!) + .should eq 1 + + parent.destroy + end + end + + describe "PUT /metadata" do + it "creates metadata" do + parent = Model::Generator.zone.save! + meta = Model::Metadata::Interface.new( + name: "test", + description: "", + details: JSON.parse(%({"hello":"world","bye":"friends"})), + parent_id: nil, + editors: Set(String).new, + ) + + parent_id = parent.id.as(String) + path = "#{Metadata.base_route}/#{parent_id}" + + result = client.put( + path: path, + body: meta.to_json, + headers: authorization_header, + ) + + result.status_code.should eq 201 + + new_metadata = Model::Metadata::Interface.from_json(result.body) + found = Model::Metadata.for(parent.id.as(String), meta.name).first + found.name.should eq new_metadata.name + end + + it "updates metadata" do + parent = Model::Generator.zone.save! + meta = Model::Metadata::Interface.new( + name: "test", + description: "", + details: JSON.parse(%({"hello":"world","bye":"friends"})), + parent_id: nil, + editors: Set(String).new, + ) + + parent_id = parent.id.as(String) + path = "#{Metadata.base_route}/#{parent_id}" + + result = client.put( + path: path, + body: meta.to_json, + headers: authorization_header, + ) + + result.status_code.should eq 201 + + new_metadata = Model::Metadata::Interface.from_json(result.body) + found = Model::Metadata.for(parent_id, meta.name).first + found.name.should eq new_metadata.name + + updated_meta = Model::Metadata::Interface.new( + name: "test", + description: "", + details: JSON.parse(%({"hello":"world"})), + parent_id: nil, + editors: Set(String).new, + ) + + result = client.put( + path: path, + body: updated_meta.to_json, + headers: authorization_header, + ) + + result.status_code.should eq 200 + + update_response_meta = Model::Metadata::Interface.from_json(result.body) + update_response_meta.details.as_h["bye"]?.should be_nil + + found = Model::Metadata.for(parent_id, meta.name).first + found.details.as_h["bye"]?.should be_nil + end + end + + describe "GET /metadata/:id" do + it "shows control_system metadata" do + control_system = Model::Generator.control_system.save! + control_system_id = control_system.id.as(String) + meta = Model::Generator.metadata(name: "special", parent: control_system_id).save! + + result = client.get( + path: "#{Metadata.base_route}/#{control_system_id}", + headers: authorization_header, + ) + + metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) + metadata.size.should eq 1 + metadata.first[1].parent_id.should eq control_system_id + metadata.first[1].name.should eq meta.name + end + + it "filters control_system metadata" do + control_system = Model::Generator.control_system.save! + control_system_id = control_system.id.as(String) + + Model::Generator.metadata(parent: control_system_id).save! + Model::Generator.metadata(name: "special", parent: control_system_id).save! + + result = client.get( + path: "#{Metadata.base_route}/#{control_system_id}?name=special", + headers: authorization_header, + ) + + metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) + metadata.size.should eq 1 + end + + it "shows zone metadata" do + zone = Model::Generator.zone.save! + zone_id = zone.id.as(String) + meta = Model::Generator.metadata(name: "special", parent: zone_id).save! + + result = client.get( + path: "#{Metadata.base_route}/#{zone_id}", + headers: authorization_header, + ) + + metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) + metadata.size.should eq 1 + metadata.first[1].parent_id.should eq zone_id + metadata.first[1].name.should eq meta.name + end + + it "filters zone metadata" do + zone = Model::Generator.zone.save! + zone_id = zone.id.as(String) + + Model::Generator.metadata(parent: zone_id).save! + Model::Generator.metadata(name: "special", parent: zone_id).save! + + result = client.get( + path: "#{Metadata.base_route}/#{zone_id}?name=special", + headers: authorization_header, + ) + + metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) + metadata.size.should eq 1 + end + end + + describe "GET /metadata/:id/history" do + it "renders the version history for a single metadata document" do + changes = [0, 1, 2, 3].map { |i| JSON::Any.new({"test" => JSON::Any.new(i.to_i64)}) } + name = random_name + metadata = Model::Generator.metadata(name: name) + metadata.details = changes.first + metadata.save! + + changes[1..].each_with_index(offset: 1) do |detail, i| + Timecop.freeze(i.seconds.from_now) do + metadata.details = detail + metadata.save! + end + end + + result = client.get( + path: File.join(Metadata.base_route, metadata.parent_id.as(String), "history"), + headers: authorization_header, + ) + + result.status_code.should eq 200 + history = Hash(String, Array(Model::Metadata::Interface)).from_json(result.body) + history.has_key?(name).should be_true + history[name].map(&.details.as_h["test"]).should eq [3, 2, 1, 0] + end + end + + describe "scopes" do + context "read" do + scope_name = "metadata" + + it "allows access to show" do + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new(scope_name, :read)]) + parent = Model::Generator.zone.save! parent_id = parent.id.as(String) @@ -19,12 +246,11 @@ module PlaceOS::Api Model::Generator.metadata(parent: child.id).save! end - result = curl( - method: "GET", - path: "#{base}/#{parent_id}/children", - headers: authorization_header, + result = client.get( + path: "#{Metadata.base_route}/#{parent_id}/children", + headers: scoped_authorization_header, ) - + result.status_code.should eq 200 Array(NamedTuple(zone: JSON::Any, metadata: Hash(String, Model::Metadata::Interface))) .from_json(result.body) .tap(&.size.should eq(3)) @@ -34,65 +260,35 @@ module PlaceOS::Api parent.destroy end - it "filters zone children metadata" do + it "should not allow access to delete" do + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new(scope_name, :read)]) + parent = Model::Generator.zone.save! parent_id = parent.id.as(String) - children = Array.new(size: 3) do + 3.times do child = Model::Generator.zone child.parent_id = parent_id child.save! Model::Generator.metadata(parent: child.id).save! - child end - # Create a single special metadata to filter on - Model::Generator.metadata(name: "special", parent: children.first.id).save! + id = parent.id.as(String) - result = curl( - method: "GET", - path: "#{base}/#{parent_id}/children?name=special", - headers: authorization_header, + result = client.delete( + path: "#{Metadata.base_route}/#{id}", + headers: scoped_authorization_header, ) - - Array(NamedTuple(zone: JSON::Any, metadata: Hash(String, Model::Metadata::Interface))) - .from_json(result.body) - .count(&.[:metadata].empty?.!) - .should eq 1 - - parent.destroy + result.status_code.should eq 403 end end - describe "PUT /metadata" do - it "creates metadata" do - parent = Model::Generator.zone.save! - meta = Model::Metadata::Interface.new( - name: "test", - description: "", - details: JSON.parse(%({"hello":"world","bye":"friends"})), - parent_id: nil, - editors: Set(String).new, - ) - - parent_id = parent.id.as(String) - path = "#{base}/#{parent_id}" + context "write" do + scope_name = "metadata" - result = curl( - method: "PUT", - path: path, - body: meta.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) + it "should allow access to update" do + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new(scope_name, :write)]) - result.status_code.should eq 201 - - new_metadata = Model::Metadata::Interface.from_json(result.body) - found = Model::Metadata.for(parent.id.as(String), meta.name).first - found.name.should eq new_metadata.name - end - - it "updates metadata" do parent = Model::Generator.zone.save! meta = Model::Metadata::Interface.new( name: "test", @@ -103,285 +299,70 @@ module PlaceOS::Api ) parent_id = parent.id.as(String) - path = "#{base}/#{parent_id}" + path = "#{Metadata.base_route}/#{parent_id}" - result = curl( - method: "PUT", + result = client.put( path: path, body: meta.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}), + headers: scoped_authorization_header, ) - result.status_code.should eq 201 - new_metadata = Model::Metadata::Interface.from_json(result.body) found = Model::Metadata.for(parent_id, meta.name).first found.name.should eq new_metadata.name - - updated_meta = Model::Metadata::Interface.new( - name: "test", - description: "", - details: JSON.parse(%({"hello":"world"})), - parent_id: nil, - editors: Set(String).new, - ) - - result = curl( - method: "PUT", - path: path, - body: updated_meta.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) - - result.status_code.should eq 200 - - update_response_meta = Model::Metadata::Interface.from_json(result.body) - update_response_meta.details.as_h["bye"]?.should be_nil - - found = Model::Metadata.for(parent_id, meta.name).first - found.details.as_h["bye"]?.should be_nil end - end - - describe "GET /metadata/:id" do - it "shows control_system metadata" do - control_system = Model::Generator.control_system.save! - control_system_id = control_system.id.as(String) - meta = Model::Generator.metadata(name: "special", parent: control_system_id).save! - - result = curl( - method: "GET", - path: "#{base}/#{control_system_id}", - headers: authorization_header, - ) - - metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) - metadata.size.should eq 1 - metadata.first[1].parent_id.should eq control_system_id - metadata.first[1].name.should eq meta.name - end - - it "filters control_system metadata" do - control_system = Model::Generator.control_system.save! - control_system_id = control_system.id.as(String) - - Model::Generator.metadata(parent: control_system_id).save! - Model::Generator.metadata(name: "special", parent: control_system_id).save! - - result = curl( - method: "GET", - path: "#{base}/#{control_system_id}?name=special", - headers: authorization_header, - ) - - metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) - metadata.size.should eq 1 - end - - it "shows zone metadata" do - zone = Model::Generator.zone.save! - zone_id = zone.id.as(String) - meta = Model::Generator.metadata(name: "special", parent: zone_id).save! - - result = curl( - method: "GET", - path: "#{base}/#{zone_id}", - headers: authorization_header, - ) - - metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) - metadata.size.should eq 1 - metadata.first[1].parent_id.should eq zone_id - metadata.first[1].name.should eq meta.name - end - - it "filters zone metadata" do - zone = Model::Generator.zone.save! - zone_id = zone.id.as(String) - Model::Generator.metadata(parent: zone_id).save! - Model::Generator.metadata(name: "special", parent: zone_id).save! + it "should not allow access to show" do + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new(scope_name, :write)]) - result = curl( - method: "GET", - path: "#{base}/#{zone_id}?name=special", - headers: authorization_header, - ) - - metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) - metadata.size.should eq 1 - end - end + parent = Model::Generator.zone.save! + parent_id = parent.id.as(String) - describe "GET /metadata/:id/history" do - it "renders the version history for a single metadata document" do - changes = [0, 1, 2, 3].map { |i| JSON::Any.new({"test" => JSON::Any.new(i.to_i64)}) } - name = random_name - metadata = Model::Generator.metadata(name: name) - metadata.details = changes.first - metadata.save! - - changes[1..].each_with_index(offset: 1) do |detail, i| - Timecop.freeze(i.seconds.from_now) do - metadata.details = detail - metadata.save! - end + 3.times do + child = Model::Generator.zone + child.parent_id = parent_id + child.save! + Model::Generator.metadata(parent: child.id).save! end - result = curl( - method: "GET", - path: File.join(base, metadata.parent_id.as(String), "history"), - headers: authorization_header, + result = client.get( + path: "#{Metadata.base_route}/#{parent_id}/children", + headers: scoped_authorization_header, ) - - result.status_code.should eq 200 - history = Hash(String, Array(Model::Metadata::Interface)).from_json(result.body) - history.has_key?(name).should be_true - history[name].map(&.details.as_h["test"]).should eq [3, 2, 1, 0] + result.status_code.should eq 403 end end - describe "scopes" do - context "read" do - scope_name = "metadata" - - it "allows access to show" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new(scope_name, :read)]) - - parent = Model::Generator.zone.save! - parent_id = parent.id.as(String) - - 3.times do - child = Model::Generator.zone - child.parent_id = parent_id - child.save! - Model::Generator.metadata(parent: child.id).save! - end - - result = curl( - method: "GET", - path: "#{base}/#{parent_id}/children", - headers: scoped_authorization_header, - ) - result.status_code.should eq 200 - Array(NamedTuple(zone: JSON::Any, metadata: Hash(String, Model::Metadata::Interface))) - .from_json(result.body) - .tap(&.size.should eq(3)) - .count(&.[:metadata].empty?.!) - .should eq 3 - - parent.destroy - end - - it "should not allow access to delete" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new(scope_name, :read)]) - - parent = Model::Generator.zone.save! - parent_id = parent.id.as(String) - - 3.times do - child = Model::Generator.zone - child.parent_id = parent_id - child.save! - Model::Generator.metadata(parent: child.id).save! - end + it "checks that guests can read metadata" do + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("guest", PlaceOS::Model::UserJWT::Scope::Access::Read)]) - id = parent.id.as(String) + zone = Model::Generator.zone.save! + zone_id = zone.id.as(String) + meta = Model::Generator.metadata(name: "special", parent: zone_id).save! - result = curl( - method: "DELETE", - path: "#{base}/#{id}", - headers: scoped_authorization_header, - ) - result.status_code.should eq 403 - end - end - - context "write" do - scope_name = "metadata" - - it "should allow access to update" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new(scope_name, :write)]) - - parent = Model::Generator.zone.save! - meta = Model::Metadata::Interface.new( - name: "test", - description: "", - details: JSON.parse(%({"hello":"world","bye":"friends"})), - parent_id: nil, - editors: Set(String).new, - ) - - parent_id = parent.id.as(String) - path = "#{base}/#{parent_id}" - - result = curl( - method: "PUT", - path: path, - body: meta.to_json, - headers: scoped_authorization_header.merge({"Content-Type" => "application/json"}), - ) - - new_metadata = Model::Metadata::Interface.from_json(result.body) - found = Model::Metadata.for(parent_id, meta.name).first - found.name.should eq new_metadata.name - end - - it "should not allow access to show" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new(scope_name, :write)]) - - parent = Model::Generator.zone.save! - parent_id = parent.id.as(String) - - 3.times do - child = Model::Generator.zone - child.parent_id = parent_id - child.save! - Model::Generator.metadata(parent: child.id).save! - end - - result = curl( - method: "GET", - path: "#{base}/#{parent_id}/children", - headers: scoped_authorization_header, - ) - result.status_code.should eq 403 - end - end - - it "checks that guests can read metadata" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("guest", PlaceOS::Model::UserJWT::Scope::Access::Read)]) - - zone = Model::Generator.zone.save! - zone_id = zone.id.as(String) - meta = Model::Generator.metadata(name: "special", parent: zone_id).save! + result = client.get( + path: "#{Metadata.base_route}/#{zone_id}", + headers: scoped_authorization_header, + ) - result = curl( - method: "GET", - path: "#{base}/#{zone_id}", - headers: scoped_authorization_header, - ) - - result.status_code.should eq 200 - metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) - metadata.size.should eq 1 - metadata.values.first.parent_id.should eq zone_id - metadata.values.first.name.should eq meta.name - end + result.status_code.should eq 200 + metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) + metadata.size.should eq 1 + metadata.values.first.parent_id.should eq zone_id + metadata.values.first.name.should eq meta.name + end - it "checks that guests cannot write metadata" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("guest", PlaceOS::Model::UserJWT::Scope::Access::Read)]) + it "checks that guests cannot write metadata" do + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("guest", PlaceOS::Model::UserJWT::Scope::Access::Read)]) - zone = Model::Generator.zone.save! - zone_id = zone.id.as(String) + zone = Model::Generator.zone.save! + zone_id = zone.id.as(String) - result = curl( - method: "POST", - path: "#{base}/#{zone_id}", - headers: scoped_authorization_header, - ) - result.success?.should be_false - end + result = client.post( + path: "#{Metadata.base_route}/#{zone_id}", + headers: scoped_authorization_header, + ) + result.success?.should be_false end end end diff --git a/spec/controllers/modules_spec.cr b/spec/controllers/modules_spec.cr index b3300c98..cddd0cbe 100644 --- a/spec/controllers/modules_spec.cr +++ b/spec/controllers/modules_spec.cr @@ -4,180 +4,164 @@ require "timecop" module PlaceOS::Api describe Modules do _authenticated_user, authorization_header = authentication - base = Modules::NAMESPACE[0] + Specs.test_404(Modules.base_route, model_name: Model::Module.table_name, headers: authorization_header) - with_server do - Specs.test_404(base, model_name: Model::Module.table_name, headers: authorization_header) + describe "CRUD operations", tags: "crud" do + Specs.test_crd(klass: Model::Module, controller_klass: Modules) - describe "CRUD operations", tags: "crud" do - Specs.test_crd(klass: Model::Module, controller_klass: Modules) - - it "update preserves logic module connection status" do - driver = Model::Generator.driver(role: Model::Driver::Role::Logic).save! - mod = Model::Generator.module(driver: driver).save! + it "update preserves logic module connection status" do + driver = Model::Generator.driver(role: Model::Driver::Role::Logic).save! + mod = Model::Generator.module(driver: driver).save! - mod.connected = false + mod.connected = false - id = mod.id.as(String) - path = File.join(base, id) + id = mod.id.as(String) + path = File.join(Modules.base_route, id) - result = curl( - method: "PATCH", - path: path, - body: mod.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) + result = client.patch( + path: path, + body: mod.to_json, + headers: authorization_header, + ) - result.status_code.should eq 200 - updated = Model::Module.from_trusted_json(result.body) - updated.id.should eq mod.id - updated.connected.should be_true - end + result.status_code.should eq 200 + updated = Model::Module.from_trusted_json(result.body) + updated.id.should eq mod.id + updated.connected.should be_true + end - it "update" do - driver = Model::Generator.driver(role: Model::Driver::Role::Service).save! - mod = Model::Generator.module(driver: driver).save! + it "update" do + driver = Model::Generator.driver(role: Model::Driver::Role::Service).save! + mod = Model::Generator.module(driver: driver).save! - connected = mod.connected - mod.connected = !connected + connected = mod.connected + mod.connected = !connected - id = mod.id.as(String) - path = File.join(base, id) + id = mod.id.as(String) + path = File.join(Modules.base_route, id) - result = curl( - method: "PATCH", - path: path, - body: mod.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) + result = client.patch( + path: path, + body: mod.to_json, + headers: authorization_header, + ) - result.status_code.should eq 200 - updated = Model::Module.from_trusted_json(result.body) - updated.id.should eq mod.id - updated.connected.should eq !connected - end + result.status_code.should eq 200 + updated = Model::Module.from_trusted_json(result.body) + updated.id.should eq mod.id + updated.connected.should eq !connected end + end - describe "index", tags: "search" do - it "queries by parent driver" do - name = random_name + describe "index", tags: "search" do + it "queries by parent driver" do + name = random_name - driver = Model::Generator.driver - driver.name = name - driver.save! + driver = Model::Generator.driver + driver.name = name + driver.save! - # Module name is dependent on the driver's name - doc = Model::Generator.module(driver: driver).save! - doc.persisted?.should be_true + # Module name is dependent on the driver's name + doc = Model::Generator.module(driver: driver).save! + doc.persisted?.should be_true - refresh_elastic(Model::Module.table_name) + refresh_elastic(Model::Module.table_name) - params = HTTP::Params.encode({"q" => name}) - path = "#{base.rstrip('/')}?#{params}" - header = authorization_header - found = until_expected("GET", path, header) do |response| - Array(Hash(String, JSON::Any)).from_json(response.body).any? do |result| - result["id"].as_s == doc.id - end + params = HTTP::Params.encode({"q" => name}) + path = "#{Modules.base_route.rstrip('/')}?#{params}" + header = authorization_header + found = until_expected("GET", path, header) do |response| + Array(Hash(String, JSON::Any)).from_json(response.body).any? do |result| + result["id"].as_s == doc.id end - - found.should be_true end - it "looks up by system_id" do - mod = Model::Generator.module.save! - sys = Model::Generator.control_system - sys.modules = [mod.id.as(String)] - sys.save! + found.should be_true + end - response_io = IO::Memory.new + it "looks up by system_id" do + mod = Model::Generator.module.save! + sys = Model::Generator.control_system + sys.modules = [mod.id.as(String)] + sys.save! - ctx = context("GET", base) - ctx.route_params = {"control_system_id" => sys.id.as(String)} - ctx.response.output = response_io + # Call the index method of the controller + response = client.get( + "#{Modules.base_route}?#{HTTP::Params{"control_system_id" => sys.id.as(String)}}" + ) - controller = Api::Modules.new(ctx, :index) + response.headers["X-Total-Count"].should eq("1") + Array(Hash(String, JSON::Any)).from_json(response.body.to_s).map(&.["id"].as_s).first?.should eq(mod.id) + end - # Call the index method of the controller - controller.index + context "query parameter" do + it "as_of" do + mod1 = Model::Generator.module + mod1.connected = true + Timecop.freeze(2.days.ago) do + mod1.save! + end + mod1.persisted?.should be_true - results = Array(Hash(String, JSON::Any)).from_json(ctx.response.output.to_s).map(&.["id"].as_s) - got_one = ctx.response.headers["X-Total-Count"] == "1" - right_one = results.first? == mod.id - found = got_one && right_one + mod2 = Model::Generator.module + mod2.connected = true + mod2.save! + mod2.persisted?.should be_true - found.should be_true - end + params = HTTP::Params.encode({"as_of" => (mod1.updated_at.try &.to_unix).to_s}) + path = "#{Modules.base_route}?#{params}" - context "query parameter" do - it "as_of" do - mod1 = Model::Generator.module - mod1.connected = true - Timecop.freeze(2.days.ago) do - mod1.save! - end - mod1.persisted?.should be_true - - mod2 = Model::Generator.module - mod2.connected = true - mod2.save! - mod2.persisted?.should be_true - - params = HTTP::Params.encode({"as_of" => (mod1.updated_at.try &.to_unix).to_s}) - path = "#{base}?#{params}" - - found = until_expected("GET", path, authorization_header) do |response| - results = Array(Hash(String, JSON::Any)).from_json(response.body).map(&.["id"].as_s) - contains_correct = results.any?(mod1.id) - contains_incorrect = results.any?(mod2.id) - !results.empty? && contains_correct && !contains_incorrect - end - - found.should be_true + found = until_expected("GET", path, authorization_header) do |response| + results = Array(Hash(String, JSON::Any)).from_json(response.body).map(&.["id"].as_s) + contains_correct = results.any?(mod1.id) + contains_incorrect = results.any?(mod2.id) + !results.empty? && contains_correct && !contains_incorrect end - it "connected" do - mod = Model::Generator.module - mod.ignore_connected = false - mod.connected = true - mod.save! - mod.persisted?.should be_true + found.should be_true + end - params = HTTP::Params.encode({"connected" => "true"}) - path = "#{base}?#{params}" + it "connected" do + mod = Model::Generator.module + mod.ignore_connected = false + mod.connected = true + mod.save! + mod.persisted?.should be_true - found = until_expected("GET", path, authorization_header) do |response| - results = Array(Hash(String, JSON::Any)).from_json(response.body) + params = HTTP::Params.encode({"connected" => "true"}) + path = "#{Modules.base_route}?#{params}" - all_connected = results.all? { |r| r["connected"].as_bool == true } - contains_created = results.any? { |r| r["id"].as_s == mod.id } + found = until_expected("GET", path, authorization_header) do |response| + results = Array(Hash(String, JSON::Any)).from_json(response.body) - !results.empty? && all_connected && contains_created - end + all_connected = results.all? { |r| r["connected"].as_bool == true } + contains_created = results.any? { |r| r["id"].as_s == mod.id } - found.should be_true + !results.empty? && all_connected && contains_created end - it "no_logic" do - driver = Model::Generator.driver(role: Model::Driver::Role::Service).save! - mod = Model::Generator.module(driver: driver) - mod.role = Model::Driver::Role::Service - mod.save! + found.should be_true + end - params = HTTP::Params.encode({"no_logic" => "true"}) - path = "#{base}?#{params}" + it "no_logic" do + driver = Model::Generator.driver(role: Model::Driver::Role::Service).save! + mod = Model::Generator.module(driver: driver) + mod.role = Model::Driver::Role::Service + mod.save! - found = until_expected("GET", path, authorization_header) do |response| - results = Array(Hash(String, JSON::Any)).from_json(response.body) + params = HTTP::Params.encode({"no_logic" => "true"}) + path = "#{Modules.base_route}?#{params}" - no_logic = results.all? { |r| r["role"].as_i != Model::Driver::Role::Logic.to_i } - contains_created = results.any? { |r| r["id"].as_s == mod.id } + found = until_expected("GET", path, authorization_header) do |response| + results = Array(Hash(String, JSON::Any)).from_json(response.body) - !results.empty? && no_logic && contains_created - end + no_logic = results.all? { |r| r["role"].as_i != Model::Driver::Role::Logic.to_i } + contains_created = results.any? { |r| r["id"].as_s == mod.id } - found.should be_true + !results.empty? && no_logic && contains_created end + + found.should be_true end end end @@ -210,9 +194,8 @@ module PlaceOS::Api driver.settings, ].flat_map(&.compact_map(&.id)).reverse! - path = "#{base}#{mod.id}/settings" - result = curl( - method: "GET", + path = "#{Modules.base_route}#{mod.id}/settings" + result = client.get( path: path, headers: authorization_header, ) @@ -237,10 +220,9 @@ module PlaceOS::Api control_system.update! mod = Model::Generator.module(driver: driver, control_system: control_system).save! - path = "#{base}#{mod.id}/settings" + path = "#{Modules.base_route}#{mod.id}/settings" - result = curl( - method: "GET", + result = client.get( path: path, headers: authorization_header, ) @@ -256,10 +238,9 @@ module PlaceOS::Api it "returns an empty array for a module without associated settings" do driver = Model::Generator.driver(role: Model::Driver::Role::Service).save! mod = Model::Generator.module(driver: driver).save! - path = "#{base}#{mod.id}/settings" + path = "#{Modules.base_route}#{mod.id}/settings" - result = curl( - method: "GET", + result = client.get( path: path, headers: authorization_header, ) @@ -271,69 +252,67 @@ module PlaceOS::Api result.success?.should be_true Array(JSON::Any).from_json(result.body).should be_empty end - end - describe "POST /:id/ping" do - it "fails for logic module" do - driver = Model::Generator.driver(role: Model::Driver::Role::Logic) - mod = Model::Generator.module(driver: driver).save! - path = "#{base}#{mod.id}/ping" - result = curl( - method: "POST", - path: path, - headers: authorization_header, - ) + describe "POST /:id/ping" do + it "fails for logic module" do + driver = Model::Generator.driver(role: Model::Driver::Role::Logic) + mod = Model::Generator.module(driver: driver).save! + path = "#{Modules.base_route}#{mod.id}/ping" + result = client.post( + path: path, + headers: authorization_header, + ) - result.success?.should be_false - result.status_code.should eq 406 - end + result.success?.should be_false + result.status_code.should eq 406 + end - it "pings a module" do - driver = Model::Generator.driver(role: Model::Driver::Role::Device) - driver.default_port = 8080 - driver.save! - mod = Model::Generator.module(driver: driver) - mod.ip = "127.0.0.1" - mod.save! + it "pings a module" do + driver = Model::Generator.driver(role: Model::Driver::Role::Device) + driver.default_port = 8080 + driver.save! + mod = Model::Generator.module(driver: driver) + mod.ip = "127.0.0.1" + mod.save! - path = "#{base}#{mod.id}/ping" - result = curl( - method: "POST", - path: path, - headers: authorization_header, - ) + path = "#{Modules.base_route}#{mod.id}/ping" + result = client.post( + path: path, + headers: authorization_header, + ) - body = JSON.parse(result.body) - result.success?.should be_true - body["pingable"].should be_true - end + body = JSON.parse(result.body) + result.success?.should be_true + body["pingable"].should be_true + end - describe "scopes" do - Specs.test_controller_scope(Modules) + describe "scopes" do + Specs.test_controller_scope(Modules) - it "checks scope on update" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("modules", PlaceOS::Model::UserJWT::Scope::Access::Write)]) - driver = Model::Generator.driver(role: Model::Driver::Role::Service).save! - mod = Model::Generator.module(driver: driver).save! + it "checks scope on update" do + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("modules", PlaceOS::Model::UserJWT::Scope::Access::Write)]) + driver = Model::Generator.driver(role: Model::Driver::Role::Service).save! + mod = Model::Generator.module(driver: driver).save! - connected = mod.connected - mod.connected = !connected + connected = mod.connected + mod.connected = !connected - id = mod.id.as(String) - path = File.join(base, id) + id = mod.id.as(String) + path = File.join(Modules.base_route, id) - result = update_route(path, mod, scoped_authorization_header) + result = Scopes.update(path, mod, scoped_authorization_header) - result.status_code.should eq 200 - updated = Model::Module.from_trusted_json(result.body) - updated.id.should eq mod.id - updated.connected.should eq !connected + result.status_code.should eq 200 + updated = Model::Module.from_trusted_json(result.body) + updated.id.should eq mod.id + updated.connected.should eq !connected - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("modules", PlaceOS::Model::UserJWT::Scope::Access::Read)]) - result = update_route(path, mod, scoped_authorization_header) + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("modules", PlaceOS::Model::UserJWT::Scope::Access::Read)]) + result = Scopes.update(path, mod, scoped_authorization_header) - result.success?.should be_false - result.status_code.should eq 403 + result.success?.should be_false + result.status_code.should eq 403 + end end end end diff --git a/spec/controllers/mqtt_spec.cr b/spec/controllers/mqtt_spec.cr index e83fe2c1..3630f147 100644 --- a/spec/controllers/mqtt_spec.cr +++ b/spec/controllers/mqtt_spec.cr @@ -2,46 +2,44 @@ require "../helper" module PlaceOS::Api describe MQTT do - with_server do - scope = [PlaceOS::Model::UserJWT::Scope.new("mqtt", :write)] - authenticated_user, _scoped_authorization_header = authentication(scope: scope) - user_jwt = PlaceOS::Model::Generator.jwt(authenticated_user, scope) + scope = [PlaceOS::Model::UserJWT::Scope.new("mqtt", :write)] + authenticated_user, _scoped_authorization_header = authentication(scope: scope) + user_jwt = PlaceOS::Model::Generator.jwt(authenticated_user, scope) - describe "MQTT Access" do - describe ".mqtt_acl_status" do - it "denies access for #{MQTT::MqttAcl::None} access" do - MQTT.mqtt_acl_status(MQTT::MqttAcl::None, user_jwt).should eq HTTP::Status::FORBIDDEN - end + describe "MQTT Access" do + describe ".mqtt_acl_status" do + it "denies access for #{MQTT::MqttAcl::None} access" do + MQTT.mqtt_acl_status(MQTT::MqttAcl::None, user_jwt).should eq HTTP::Status::FORBIDDEN + end - it "denies access for #{MQTT::MqttAcl::Deny} access" do - MQTT::MqttAcl - .values - .reject(MQTT::MqttAcl::Deny) - .map { |access| access | MQTT::MqttAcl::Deny } - .each do |access| - MQTT.mqtt_acl_status(access, user_jwt).should eq HTTP::Status::FORBIDDEN - end - end + it "denies access for #{MQTT::MqttAcl::Deny} access" do + MQTT::MqttAcl + .values + .reject(MQTT::MqttAcl::Deny) + .map { |access| access | MQTT::MqttAcl::Deny } + .each do |access| + MQTT.mqtt_acl_status(access, user_jwt).should eq HTTP::Status::FORBIDDEN + end + end - it "allows #{MQTT::MqttAcl::Read} access" do - scope = [PlaceOS::Model::UserJWT::Scope.new("mqtt", :read)] - authenticated_user, _scoped_authorization_header = authentication(scope: scope) - user_jwt = PlaceOS::Model::Generator.jwt(authenticated_user, scope) - MQTT.mqtt_acl_status(MQTT::MqttAcl::Read, user_jwt).should eq HTTP::Status::OK - end + it "allows #{MQTT::MqttAcl::Read} access" do + scope = [PlaceOS::Model::UserJWT::Scope.new("mqtt", :read)] + authenticated_user, _scoped_authorization_header = authentication(scope: scope) + user_jwt = PlaceOS::Model::Generator.jwt(authenticated_user, scope) + MQTT.mqtt_acl_status(MQTT::MqttAcl::Read, user_jwt).should eq HTTP::Status::OK + end - it "allows #{MQTT::MqttAcl::Write} access for support and above" do - scope = [PlaceOS::Model::UserJWT::Scope.new("mqtt", :write)] - authenticated_user, _scoped_authorization_header = authentication(scope: scope) - user_jwt = PlaceOS::Model::Generator.jwt(authenticated_user, scope) - MQTT.mqtt_acl_status(MQTT::MqttAcl::Write, user_jwt).should eq HTTP::Status::OK - end + it "allows #{MQTT::MqttAcl::Write} access for support and above" do + scope = [PlaceOS::Model::UserJWT::Scope.new("mqtt", :write)] + authenticated_user, _scoped_authorization_header = authentication(scope: scope) + user_jwt = PlaceOS::Model::Generator.jwt(authenticated_user, scope) + MQTT.mqtt_acl_status(MQTT::MqttAcl::Write, user_jwt).should eq HTTP::Status::OK + end - it "denies #{MQTT::MqttAcl::Write} access for under support" do - authenticated_user, _ = authentication(sys_admin: false, support: false, scope: scope) - user_jwt = PlaceOS::Model::Generator.jwt(authenticated_user, scope) - MQTT.mqtt_acl_status(MQTT::MqttAcl::Write, user_jwt).should eq HTTP::Status::FORBIDDEN - end + it "denies #{MQTT::MqttAcl::Write} access for under support" do + authenticated_user, _ = authentication(sys_admin: false, support: false, scope: scope) + user_jwt = PlaceOS::Model::Generator.jwt(authenticated_user, scope) + MQTT.mqtt_acl_status(MQTT::MqttAcl::Write, user_jwt).should eq HTTP::Status::FORBIDDEN end end end diff --git a/spec/controllers/repositories_spec.cr b/spec/controllers/repositories_spec.cr index df0ba57f..2e9a0b1d 100644 --- a/spec/controllers/repositories_spec.cr +++ b/spec/controllers/repositories_spec.cr @@ -3,146 +3,130 @@ require "../helper" module PlaceOS::Api describe Repositories do _authenticated_user, authorization_header = authentication - base = Repositories::NAMESPACE[0] - with_server do - Specs.test_404(base, model_name: Model::Repository.table_name, headers: authorization_header) + Specs.test_404(Repositories.base_route, model_name: Model::Repository.table_name, headers: authorization_header) - describe "index", tags: "search" do - Specs.test_base_index(Model::Repository, Repositories) + describe "index", tags: "search" do + Specs.test_base_index(Model::Repository, Repositories) + end + + describe "CRUD operations", tags: "crud" do + Specs.test_crd(Model::Repository, Repositories) + + it "update" do + repository = Model::Generator.repository.save! + original_name = repository.name + repository.name = random_name + + id = repository.id.as(String) + path = File.join(Repositories.base_route, id) + result = client.patch( + path: path, + body: repository.changed_attributes.to_json, + headers: authorization_header, + ) + + result.status_code.should eq 200 + updated = Model::Repository.from_trusted_json(result.body) + + updated.id.should eq repository.id + updated.name.should_not eq original_name end - describe "CRUD operations", tags: "crud" do - Specs.test_crd(Model::Repository, Repositories) + describe "mutating URIs" do + it "does not update Driver repositories with modified URIs" do + repository = Model::Generator.repository(type: Model::Repository::Type::Driver).save! - it "update" do - repository = Model::Generator.repository.save! - original_name = repository.name - repository.name = random_name + id = repository.id.as(String) + path = File.join(Repositories.base_route, id) + result = client.patch( + path: path, + body: {uri: "https://changed:8080"}.to_json, + headers: authorization_header, + ) + + result.status_code.should eq 422 + end + + it "does update Interface repositories with modified URIs" do + repository = Model::Generator.repository(type: Model::Repository::Type::Interface).save! id = repository.id.as(String) - path = File.join(base, id) - result = curl( - method: "PATCH", + path = File.join(Repositories.base_route, id) + result = client.patch( path: path, - body: repository.changed_attributes.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}), + body: {uri: "https://changed:8080"}.to_json, + headers: authorization_header, ) result.status_code.should eq 200 - updated = Model::Repository.from_trusted_json(result.body) + end + end + + describe "driver only actions" do + repo = Model::Generator.repository(type: :interface) + before_all do + repo.save! + end + + it "errors if enumerating drivers in an interface repo" do + id = repo.id.as(String) + path = "#{Repositories.base_route}#{id}/drivers" + result = client.get( + path: path, + headers: authorization_header, + ) - updated.id.should eq repository.id - updated.name.should_not eq original_name + result.status.should eq HTTP::Status::BAD_REQUEST end - describe "mutating URIs" do - it "does not update Driver repositories with modified URIs" do - repository = Model::Generator.repository(type: Model::Repository::Type::Driver).save! - - id = repository.id.as(String) - path = File.join(base, id) - result = curl( - method: "PATCH", - path: path, - body: {uri: "https://changed:8080"}.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) - - result.status_code.should eq 422 - end - - it "does update Interface repositories with modified URIs" do - repository = Model::Generator.repository(type: Model::Repository::Type::Interface).save! - - id = repository.id.as(String) - path = File.join(base, id) - result = curl( - method: "PATCH", - path: path, - body: {uri: "https://changed:8080"}.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) - - result.status_code.should eq 200 - end + it "errors when requesting driver details from an interface repo" do + id = repo.id.as(String) + path = "#{Repositories.base_route}#{id}/details" + result = client.get( + path: path, + headers: authorization_header, + ) + + result.status.should eq HTTP::Status::BAD_REQUEST end + end + end - describe "driver only actions" do - repo = Model::Generator.repository(type: :interface) - before_all do - repo.save! - end - - it "errors if enumerating drivers in an interface repo" do - id = repo.id.as(String) - path = "#{base}#{id}/drivers" - result = curl( - method: "GET", - path: path, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) - - result.status.should eq HTTP::Status::BAD_REQUEST - end - - it "errors when requesting driver details from an interface repo" do - id = repo.id.as(String) - path = "#{base}#{id}/details" - result = curl( - method: "GET", - path: path, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) - - result.status.should eq HTTP::Status::BAD_REQUEST - end + describe "GET /:id/commits" do + context "interface" do + pending "fetches the commits for a repository" do end end - describe "GET /:id/commits" do - context "interface" do - pending "fetches the commits for a repository" do - end + context "driver" do + repo = Model::Generator.repository(type: :driver).tap do |r| + r.uri = "https://github.com/placeOS/private-drivers" end - context "driver" do - repo = Model::Generator.repository(type: :driver).tap do |r| - r.uri = "https://github.com/placeOS/private-drivers" - end - - before_all do - repo.save! - end - - it "fetches the commits for a repository" do - id = repo.id.as(String) - response = Api::Repositories - .with_request("GET", "#{base}#{id}/commits", route_params: {"id" => id}, &.commits) - .response - response.status.should eq HTTP::Status::OK - Array(String).from_json(response.output).should_not be_empty - end - - it "fetches the commits for a file" do - id = repo.id.as(String) - params = HTTP::Params{ - "driver" => "drivers/place/private_helper.cr", - } - response = Api::Repositories - .with_request("GET", "#{base}#{id}/commits?#{params}", route_params: {"id" => id}, &.commits) - .response - - response.status.should eq HTTP::Status::OK - Array(String).from_json(response.output).should_not be_empty - end + before_all do + repo.save! + end + + pending "fetches commits for a repository" do + id = repo.id.as(String) + response = client.get("#{Repositories.base_route}#{id}/commits?#{HTTP::Params{"id" => id}}") + response.status.should eq HTTP::Status::OK + Array(String).from_json(response.body).should_not be_empty end - end - describe "scopes" do - Specs.test_controller_scope(Repositories) - Specs.test_update_write_scope(Repositories) + pending "fetches commits for a file" do + id = repo.id.as(String) + response = client.get("#{Repositories.base_route}#{id}/commits?#{HTTP::Params{"driver" => "drivers/place/private_helper.cr", "id" => id}}") + response.status.should eq HTTP::Status::OK + Array(String).from_json(response.body).should_not be_empty + end end end + + describe "scopes" do + Specs.test_controller_scope(Repositories) + Specs.test_update_write_scope(Repositories) + end end end diff --git a/spec/controllers/root_spec.cr b/spec/controllers/root_spec.cr index 9f966473..5accea4f 100644 --- a/spec/controllers/root_spec.cr +++ b/spec/controllers/root_spec.cr @@ -2,62 +2,98 @@ require "../helper" module PlaceOS::Api describe Root do - with_server do - _authenticated_user, authorization_header = authentication - base = Api::Root::NAMESPACE[0] + _authenticated_user, authorization_header = authentication - describe "GET /" do - it "responds to health checks" do - result = curl("GET", base, headers: authorization_header) - result.status_code.should eq 200 - end + describe "GET /" do + it "responds to health checks" do + result = client.get(Root.base_route, headers: authorization_header) + result.status_code.should eq 200 end + end - describe "GET /scopes" do - it "gets scope names" do - result = curl("GET", File.join(base, "scopes"), headers: authorization_header) - scopes = Array(String).from_json(result.body) - scopes.size.should eq(Root.scopes.size) - end + describe "GET /scopes" do + it "gets scope names" do + result = client.get(File.join(Root.base_route, "scopes"), headers: authorization_header) + scopes = Array(String).from_json(result.body) + scopes.size.should eq(Root.scopes.size) end + end - describe "GET /cluster/versions" do - it "constructs service versions" do - HttpMocks.service_version + describe "GET /cluster/versions" do + it "constructs service versions" do + HttpMocks.service_version - versions = Root.construct_versions - versions.size.should eq(Root::SERVICES.size) - versions.map(&.service.gsub('-', '_')).sort!.should eq Root::SERVICES.sort - end + versions = Root.construct_versions + versions.size.should eq(Root::SERVICES.size) + versions.map(&.service.gsub('-', '_')).sort!.should eq Root::SERVICES.sort end + end - describe "GET /version" do - it "renders version" do - result = curl("GET", File.join(base, "version"), headers: authorization_header) - result.status_code.should eq 200 - response = PlaceOS::Model::Version.from_json(result.body) + describe "GET /version" do + it "renders version" do + result = client.get(File.join(Root.base_route, "version"), headers: authorization_header) + result.status_code.should eq 200 + response = PlaceOS::Model::Version.from_json(result.body) - response.service.should eq APP_NAME - response.version.should eq VERSION - response.build_time.should eq BUILD_TIME - response.commit.should eq BUILD_COMMIT - end + response.service.should eq APP_NAME + response.version.should eq VERSION + response.build_time.should eq BUILD_TIME + response.commit.should eq BUILD_COMMIT end + end - describe "GET /platform" do - it "renders platform information" do - result = curl("GET", File.join(base, "platform"), headers: authorization_header) - result.status_code.should eq 200 - response = PlaceOS::Api::Root::PlatformInfo.from_json(result.body) + describe "GET /platform" do + it "renders platform information" do + result = client.get(File.join(Root.base_route, "platform"), headers: authorization_header) + result.status_code.should eq 200 + response = PlaceOS::Api::Root::PlatformInfo.from_json(result.body) + + response.version.should eq PLATFORM_VERSION + response.changelog.should eq PLATFORM_CHANGELOG + end + end + + describe "POST /signal" do + it "writes an arbitrary payload to a redis subscription" do + subscription_channel = "test" + channel = Channel(String).new + subs = PlaceOS::Driver::Subscriptions.new + + _subscription = subs.channel subscription_channel do |_, message| + channel.send(message) + end + + params = HTTP::Params{"channel" => subscription_channel} + result = client.post(File.join(Root.base_route, "signal?#{params}"), body: "hello", headers: authorization_header) + result.status_code.should eq 200 - response.version.should eq PLATFORM_VERSION - response.changelog.should eq PLATFORM_CHANGELOG + begin + select + when message = channel.receive + message.should eq "hello" + when timeout 2.seconds + raise "timeout" + end + ensure + subs.terminate end end - describe "POST /signal" do - it "writes an arbitrary payload to a redis subscription" do - subscription_channel = "test" + it "validates presence of `channel` param" do + result = client.post(File.join(Root.base_route, "signal"), body: "hello", headers: authorization_header) + result.status_code.should eq 400 + end + + context "guest users" do + _, guest_header = authentication(sys_admin: false, support: false, scope: [PlaceOS::Model::UserJWT::Scope::GUEST]) + + it "prevented access to non-guest channels " do + result = client.post(File.join(Root.base_route, "signal?channel=dummy"), body: "hello", headers: guest_header) + result.status_code.should eq 403 + end + + it "allowed access to guest channels" do + subscription_channel = "/guest/dummy" channel = Channel(String).new subs = PlaceOS::Driver::Subscriptions.new @@ -66,7 +102,7 @@ module PlaceOS::Api end params = HTTP::Params{"channel" => subscription_channel} - result = curl("POST", File.join(base, "signal?#{params}"), body: "hello", headers: authorization_header) + result = client.post(File.join(Root.base_route, "signal?#{params}"), body: "hello", headers: guest_header) result.status_code.should eq 200 begin @@ -80,45 +116,6 @@ module PlaceOS::Api subs.terminate end end - - it "validates presence of `channel` param" do - result = curl("POST", File.join(base, "signal"), body: "hello", headers: authorization_header) - result.status_code.should eq 400 - end - - context "guest users" do - _, guest_header = authentication(sys_admin: false, support: false, scope: [PlaceOS::Model::UserJWT::Scope::GUEST]) - - it "prevented access to non-guest channels " do - result = curl("POST", File.join(base, "signal?channel=dummy"), body: "hello", headers: guest_header) - result.status_code.should eq 403 - end - - it "allowed access to guest channels" do - subscription_channel = "/guest/dummy" - channel = Channel(String).new - subs = PlaceOS::Driver::Subscriptions.new - - _subscription = subs.channel subscription_channel do |_, message| - channel.send(message) - end - - params = HTTP::Params{"channel" => subscription_channel} - result = curl("POST", File.join(base, "signal?#{params}"), body: "hello", headers: guest_header) - result.status_code.should eq 200 - - begin - select - when message = channel.receive - message.should eq "hello" - when timeout 2.seconds - raise "timeout" - end - ensure - subs.terminate - end - end - end end end end diff --git a/spec/controllers/settings_spec.cr b/spec/controllers/settings_spec.cr index 796314f4..320e7317 100644 --- a/spec/controllers/settings_spec.cr +++ b/spec/controllers/settings_spec.cr @@ -3,227 +3,216 @@ require "../helper" module PlaceOS::Api describe Settings do _, authorization_header = authentication - base = Api::Settings::NAMESPACE[0] - with_server do - Specs.test_404(base, model_name: Model::Settings.table_name, headers: authorization_header) - - describe "support user" do - context "access" do - it "index" do - _, support_header = authentication(sys_admin: false, support: true) - sys = Model::Generator.control_system.save! - setting = Model::Generator.settings(encryption_level: Encryption::Level::None, control_system: sys) - setting.settings_string = "tree: 1" - setting.save! - result = curl( - method: "GET", - path: File.join(base, "?parent_id=#{sys.id}"), - headers: support_header, - ) - - result.status_code.should eq 200 - end + Specs.test_404(Settings.base_route, model_name: Model::Settings.table_name, headers: authorization_header) - it "show" do - _, support_header = authentication(sys_admin: false, support: true) - sys = Model::Generator.control_system.save! - setting = Model::Generator.settings(encryption_level: Encryption::Level::None, control_system: sys) - setting.settings_string = "tree: 1" - setting.save! - result = curl( - method: "GET", - path: File.join(base, setting.id.as(String)), - headers: support_header, - ) - - result.status_code.should eq 200 - end - end - end - - describe "index", tags: "search" do - it "searches on keys" do - unencrypted = %({"secret_key": "secret1234"}) - settings = Model::Generator.settings(settings_string: unencrypted).save! - - sleep 1.seconds - refresh_elastic(Model::Settings.table_name) + describe "support user" do + context "access" do + it "index" do + _, support_header = authentication(sys_admin: false, support: true) + sys = Model::Generator.control_system.save! + setting = Model::Generator.settings(encryption_level: Encryption::Level::None, control_system: sys) + setting.settings_string = "tree: 1" + setting.save! + result = client.get( + path: File.join(Settings.base_route, "?parent_id=#{sys.id}"), + headers: support_header, + ) - params = HTTP::Params.encode({"q" => settings.keys.first}) - path = "#{base.rstrip('/')}?#{params}" + result.status_code.should eq 200 + end - result = curl( - method: "GET", - path: path, - headers: authorization_header + it "show" do + _, support_header = authentication(sys_admin: false, support: true) + sys = Model::Generator.control_system.save! + setting = Model::Generator.settings(encryption_level: Encryption::Level::None, control_system: sys) + setting.settings_string = "tree: 1" + setting.save! + result = client.get( + path: File.join(Settings.base_route, setting.id.as(String)), + headers: support_header, ) result.status_code.should eq 200 - - settings = Array(Model::Settings).from_json(result.body) - settings.should_not be_empty - settings.first.keys.should contain("secret_key") end + end + end - it "returns settings for a set of parent ids" do - systems = Array.new(2) { Model::Generator.control_system.save! } + describe "index", tags: "search" do + it "searches on keys" do + unencrypted = %({"secret_key": "secret1234"}) + settings = Model::Generator.settings(settings_string: unencrypted).save! - systems.map do |system| - {Encryption::Level::None, Encryption::Level::Admin, Encryption::Level::NeverDisplay}.map do |level| - Model::Generator.settings(encryption_level: level, control_system: system).save! - end - end + sleep 1.seconds + refresh_elastic(Model::Settings.table_name) - sys, sys2 = systems + params = HTTP::Params.encode({"q" => settings.keys.first}) + path = "#{Settings.base_route.rstrip('/')}?#{params}" - refresh_elastic(Model::Settings.table_name) + result = client.get( + path: path, + headers: authorization_header + ) - result = curl( - method: "GET", - path: File.join(base, "?parent_id=#{sys.id},#{sys2.id}"), - headers: authorization_header - ) + result.status_code.should eq 200 - result.status_code.should eq 200 + settings = Array(Model::Settings).from_json(result.body) + settings.should_not be_empty + settings.first.keys.should contain("secret_key") + end - returned_settings = Array(Model::Settings).from_json(result.body) + it "returns settings for a set of parent ids" do + systems = Array.new(2) { Model::Generator.control_system.save! } - returned_settings.size.should eq(6) + systems.map do |system| + {Encryption::Level::None, Encryption::Level::Admin, Encryption::Level::NeverDisplay}.map do |level| + Model::Generator.settings(encryption_level: level, control_system: system).save! + end + end - never_displayed_settings, admin_settings, no_encryption_settings = returned_settings.in_groups_of(2).map(&.compact) + sys, sys2 = systems - never_displayed_settings.all?(&.encryption_level.never_display?).should be_true - admin_settings.all?(&.encryption_level.admin?).should be_true - no_encryption_settings.all?(&.encryption_level.none?).should be_true - end + refresh_elastic(Model::Settings.table_name) - it "returns settings for parent id" do - sys = Model::Generator.control_system.save! - settings = [ - Model::Generator.settings(encryption_level: Encryption::Level::None, control_system: sys), - Model::Generator.settings(encryption_level: Encryption::Level::Admin, control_system: sys), - Model::Generator.settings(encryption_level: Encryption::Level::NeverDisplay, control_system: sys), - ] - clear, admin, never_displayed = settings.map(&.save!) - refresh_elastic(Model::Settings.table_name) - - result = curl( - method: "GET", - path: File.join(base, "?parent_id=#{sys.id}"), - headers: authorization_header - ) + result = client.get( + path: File.join(Settings.base_route, "?parent_id=#{sys.id},#{sys2.id}"), + headers: authorization_header + ) - result.status_code.should eq 200 + result.status_code.should eq 200 - returned_settings = Array(JSON::Any) - .from_json(result.body) - .map { |j| Model::Settings.from_trusted_json(j.to_json) } - .sort_by!(&.encryption_level) + returned_settings = Array(Model::Settings).from_json(result.body) - returned_clear, returned_admin, returned_never_displayed = returned_settings + returned_settings.size.should eq(6) - returned_clear.id.should eq clear.id - returned_admin.id.should eq admin.id - returned_never_displayed.id.should eq never_displayed.id + never_displayed_settings, admin_settings, no_encryption_settings = returned_settings.in_groups_of(2).map(&.compact) - returned_clear.is_encrypted?.should be_false - returned_admin.is_encrypted?.should be_false - returned_never_displayed.is_encrypted?.should be_true - end + never_displayed_settings.all?(&.encryption_level.never_display?).should be_true + admin_settings.all?(&.encryption_level.admin?).should be_true + no_encryption_settings.all?(&.encryption_level.none?).should be_true end - describe "GET /settings/:id/history" do - it "returns history for a master setting" do - sys = Model::Generator.control_system.save! - - setting = Model::Generator.settings(encryption_level: Encryption::Level::None, control_system: sys) - setting.settings_string = "tree: 1" - setting.save! + it "returns settings for parent id" do + sys = Model::Generator.control_system.save! + settings = [ + Model::Generator.settings(encryption_level: Encryption::Level::None, control_system: sys), + Model::Generator.settings(encryption_level: Encryption::Level::Admin, control_system: sys), + Model::Generator.settings(encryption_level: Encryption::Level::NeverDisplay, control_system: sys), + ] + clear, admin, never_displayed = settings.map(&.save!) + refresh_elastic(Model::Settings.table_name) + + result = client.get( + path: File.join(Settings.base_route, "?parent_id=#{sys.id}"), + headers: authorization_header + ) + + result.status_code.should eq 200 + + returned_settings = Array(JSON::Any) + .from_json(result.body) + .map { |j| Model::Settings.from_trusted_json(j.to_json) } + .sort_by!(&.encryption_level) + + returned_clear, returned_admin, returned_never_displayed = returned_settings + + returned_clear.id.should eq clear.id + returned_admin.id.should eq admin.id + returned_never_displayed.id.should eq never_displayed.id + + returned_clear.is_encrypted?.should be_false + returned_admin.is_encrypted?.should be_false + returned_never_displayed.is_encrypted?.should be_true + end + end - Timecop.freeze(3.seconds.from_now) do - setting.settings_string = "tree: 10" - setting.save! - end + describe "GET /settings/:id/history" do + it "returns history for a master setting" do + sys = Model::Generator.control_system.save! - result = curl( - method: "GET", - path: File.join(base, "/#{setting.id}/history"), - headers: authorization_header - ) + setting = Model::Generator.settings(encryption_level: Encryption::Level::None, control_system: sys) + setting.settings_string = "tree: 1" + setting.save! - result.success?.should be_true - result.headers["X-Total-Count"].should eq "2" - result.headers["Content-Range"].should eq "sets 0-2/2" + Timecop.freeze(3.seconds.from_now) do + setting.settings_string = "tree: 10" + setting.save! + end - Array(JSON::Any).from_json(result.body).size.should eq 2 + result = client.get( + path: File.join(Settings.base_route, "/#{setting.id}/history"), + headers: authorization_header + ) - result = curl( - method: "GET", - path: File.join(base, "/#{setting.id}/history?limit=1"), - headers: authorization_header - ) + result.success?.should be_true + result.headers["X-Total-Count"].should eq "2" + result.headers["Content-Range"].should eq "sets 0-2/2" - link = %(; rel="next") - result.success?.should be_true - result.headers["X-Total-Count"].should eq "2" - result.headers["Content-Range"].should eq "sets 0-1/2" - result.headers["Link"].should eq link + Array(JSON::Any).from_json(result.body).size.should eq 2 - {sys, setting}.each &.destroy - end - end + result = client.get( + path: File.join(Settings.base_route, "/#{setting.id}/history?limit=1"), + headers: authorization_header + ) - describe "CRUD operations", tags: "crud" do - Specs.test_crd(klass: Model::Settings, controller_klass: Settings) - it "update" do - settings = Model::Generator.settings(encryption_level: Encryption::Level::None).save! - original_settings = settings.settings_string - settings.settings_string = %(hello: "world"\n) - - id = settings.id.as(String) - path = File.join(base, id) - result = curl( - method: "PATCH", - path: path, - body: settings.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) + link = %(; rel="next") + result.success?.should be_true + result.headers["X-Total-Count"].should eq "2" + result.headers["Content-Range"].should eq "sets 0-1/2" + result.headers["Link"].should eq link - result.status_code.should eq 200 - updated = Model::Settings.from_trusted_json(result.body) + {sys, setting}.each &.destroy + end + end - updated.id.should eq settings.id - updated.settings_string.should_not eq original_settings - updated.destroy - end + describe "CRUD operations", tags: "crud" do + Specs.test_crd(klass: Model::Settings, controller_klass: Settings) + it "update" do + settings = Model::Generator.settings(encryption_level: Encryption::Level::None).save! + original_settings = settings.settings_string + settings.settings_string = %(hello: "world"\n) + + id = settings.id.as(String) + path = File.join(Settings.base_route, id) + result = client.patch( + path: path, + body: settings.to_json, + headers: authorization_header, + ) + + result.status_code.should eq 200 + updated = Model::Settings.from_trusted_json(result.body) + + updated.id.should eq settings.id + updated.settings_string.should_not eq original_settings + updated.destroy end + end - describe "scopes" do - Specs.test_controller_scope(Settings) + describe "scopes" do + Specs.test_controller_scope(Settings) - it "checks scope on update" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("settings", PlaceOS::Model::UserJWT::Scope::Access::Write)]) - settings = Model::Generator.settings(encryption_level: Encryption::Level::None).save! - original_settings = settings.settings_string - settings.settings_string = %(hello: "world"\n) + it "checks scope on update" do + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("settings", PlaceOS::Model::UserJWT::Scope::Access::Write)]) + settings = Model::Generator.settings(encryption_level: Encryption::Level::None).save! + original_settings = settings.settings_string + settings.settings_string = %(hello: "world"\n) - id = settings.id.as(String) - path = File.join(base, id) - result = update_route(path, settings, scoped_authorization_header) + id = settings.id.as(String) + path = File.join(Settings.base_route, id) + result = Scopes.update(path, settings, scoped_authorization_header) - result.status_code.should eq 200 - updated = Model::Settings.from_trusted_json(result.body) + result.status_code.should eq 200 + updated = Model::Settings.from_trusted_json(result.body) - updated.id.should eq settings.id - updated.settings_string.should_not eq original_settings - updated.destroy + updated.id.should eq settings.id + updated.settings_string.should_not eq original_settings + updated.destroy - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("settings", PlaceOS::Model::UserJWT::Scope::Access::Read)]) - result = update_route(path, settings, scoped_authorization_header) + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("settings", PlaceOS::Model::UserJWT::Scope::Access::Read)]) + result = Scopes.update(path, settings, scoped_authorization_header) - result.success?.should be_false - result.status_code.should eq 403 - end + result.success?.should be_false + result.status_code.should eq 403 end end end diff --git a/spec/controllers/system-triggers_spec.cr b/spec/controllers/system-triggers_spec.cr index 5517a8c3..b1677f23 100644 --- a/spec/controllers/system-triggers_spec.cr +++ b/spec/controllers/system-triggers_spec.cr @@ -4,132 +4,127 @@ require "timecop" module PlaceOS::Api describe SystemTriggers do _, authorization_header = authentication - base = SystemTriggers::NAMESPACE[0] - - with_server do - Specs.test_404( - base.gsub(/:sys_id/, "sys-#{Random.rand(9999)}"), - model_name: Model::TriggerInstance.table_name, - headers: authorization_header, - ) - - describe "index", tags: "search" do - context "query parameter" do - it "as_of" do - sys = Model::Generator.control_system.save! - path = base.gsub(/:sys_id/, sys.id) - - inst1 = Model::Generator.trigger_instance - inst1.control_system = sys - Timecop.freeze(2.days.ago) do - inst1.save! - end - inst1.persisted?.should be_true - - inst2 = Model::Generator.trigger_instance - inst2.control_system = sys - inst2.save! - inst2.persisted?.should be_true - - refresh_elastic(Model::TriggerInstance.table_name) - - params = HTTP::Params.encode({"as_of" => (inst1.updated_at.try &.to_unix).to_s}) - path = "#{path}?#{params}" - correct_response = until_expected("GET", path, authorization_header) do |response| - results = Array(Hash(String, JSON::Any)).from_json(response.body).map(&.["id"].as_s) - contains_correct = results.any?(inst1.id) - contains_incorrect = results.any?(inst2.id) - - !results.empty? && contains_correct && !contains_incorrect - end - - correct_response.should be_true + + Specs.test_404( + SystemTriggers.base_route.gsub(/:sys_id/, "sys-#{Random.rand(9999)}"), + model_name: Model::TriggerInstance.table_name, + headers: authorization_header, + ) + + describe "index", tags: "search" do + context "query parameter" do + it "as_of" do + sys = Model::Generator.control_system.save! + path = SystemTriggers.base_route.gsub(/:sys_id/, sys.id) + + inst1 = Model::Generator.trigger_instance + inst1.control_system = sys + Timecop.freeze(2.days.ago) do + inst1.save! end + inst1.persisted?.should be_true + + inst2 = Model::Generator.trigger_instance + inst2.control_system = sys + inst2.save! + inst2.persisted?.should be_true + + refresh_elastic(Model::TriggerInstance.table_name) + + params = HTTP::Params.encode({"as_of" => (inst1.updated_at.try &.to_unix).to_s}) + path = "#{path}?#{params}" + correct_response = until_expected("GET", path, authorization_header) do |response| + results = Array(Hash(String, JSON::Any)).from_json(response.body).map(&.["id"].as_s) + contains_correct = results.any?(inst1.id) + contains_incorrect = results.any?(inst2.id) + + !results.empty? && contains_correct && !contains_incorrect + end + + correct_response.should be_true end end + end - describe "CRUD operations", tags: "crud" do - it "create" do - sys = Model::Generator.control_system.save! - trigger_instance = Model::Generator.trigger_instance - trigger_instance.control_system = sys - body = trigger_instance.to_json - - path = base.gsub(/:sys_id/, sys.id) - result = curl( - method: "POST", - path: path, - body: body, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) - - result.status_code.should eq 201 - body = result.body.not_nil! - Model::TriggerInstance.find(JSON.parse(body)["id"].as_s).try &.destroy - end + describe "CRUD operations", tags: "crud" do + it "create" do + sys = Model::Generator.control_system.save! + trigger_instance = Model::Generator.trigger_instance + trigger_instance.control_system = sys + body = trigger_instance.to_json + + path = SystemTriggers.base_route.gsub(/:sys_id/, sys.id) + result = client.post( + path: path, + body: body, + headers: authorization_header, + ) + + result.status_code.should eq 201 + body = result.body.not_nil! + Model::TriggerInstance.find(JSON.parse(body)["id"].as_s).try &.destroy + end - it "show" do - sys = Model::Generator.control_system.save! - trigger_instance = Model::Generator.trigger_instance - trigger_instance.control_system = sys - trigger_instance.save! - id = trigger_instance.id.not_nil! + it "show" do + sys = Model::Generator.control_system.save! + trigger_instance = Model::Generator.trigger_instance + trigger_instance.control_system = sys + trigger_instance.save! + id = trigger_instance.id.not_nil! - path = base.gsub(/:sys_id/, sys.id) + id - result = curl(method: "GET", path: path, headers: authorization_header) + path = SystemTriggers.base_route.gsub(/:sys_id/, sys.id) + id + result = client.get(path: path, headers: authorization_header) - result.status_code.should eq 200 + result.status_code.should eq 200 - response_model = Model::TriggerInstance.from_trusted_json(result.body) - response_model.id.should eq id + response_model = Model::TriggerInstance.from_trusted_json(result.body) + response_model.id.should eq id - sys.destroy - trigger_instance.destroy - end + sys.destroy + trigger_instance.destroy + end - it "update" do - sys = Model::Generator.control_system.save! - trigger_instance = Model::Generator.trigger_instance - trigger_instance.control_system = sys - trigger_instance.save! + it "update" do + sys = Model::Generator.control_system.save! + trigger_instance = Model::Generator.trigger_instance + trigger_instance.control_system = sys + trigger_instance.save! - original_importance = trigger_instance.important - updated_importance = !original_importance + original_importance = trigger_instance.important + updated_importance = !original_importance - id = trigger_instance.id.not_nil! - path = base.gsub(/:sys_id/, sys.id) + id + id = trigger_instance.id.not_nil! + path = SystemTriggers.base_route.gsub(/:sys_id/, sys.id) + id - result = curl( - method: "PATCH", - path: path, - body: {important: updated_importance}.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) + result = client.patch( + path: path, + body: {important: updated_importance}.to_json, + headers: authorization_header, + ) - result.status_code.should eq 200 - updated = Model::TriggerInstance.from_trusted_json(result.body) + result.status_code.should eq 200 + updated = Model::TriggerInstance.from_trusted_json(result.body) - updated.id.should eq trigger_instance.id - updated.important.should_not eq original_importance - updated.destroy - end + updated.id.should eq trigger_instance.id + updated.important.should_not eq original_importance + updated.destroy + end - it "destroy" do - sys = PlaceOS::Model::Generator.control_system.save! - model = PlaceOS::Model::Generator.trigger_instance - model.control_system = sys + it "destroy" do + sys = PlaceOS::Model::Generator.control_system.save! + model = PlaceOS::Model::Generator.trigger_instance + model.control_system = sys - model.save! - model.persisted?.should be_true + model.save! + model.persisted?.should be_true - id = model.id.not_nil! - path = base.gsub(/:sys_id/, sys.id) + id + id = model.id.not_nil! + path = SystemTriggers.base_route.gsub(/:sys_id/, sys.id) + id - result = curl(method: "DELETE", path: path, headers: authorization_header) - result.status_code.should eq 200 + result = client.delete(path: path, headers: authorization_header) + result.status_code.should eq 200 - Model::TriggerInstance.find(id.as(String)).should be_nil - end + Model::TriggerInstance.find(id.as(String)).should be_nil end end end diff --git a/spec/controllers/systems_spec.cr b/spec/controllers/systems_spec.cr index 6839aa6a..3050c6ca 100644 --- a/spec/controllers/systems_spec.cr +++ b/spec/controllers/systems_spec.cr @@ -1,14 +1,13 @@ -require "../helper" -require "../core_helper" require "http/web_socket" +require "../helper" + module PlaceOS::Api def self.spec_add_module(system, mod, headers) mod_id = mod.id.as(String) path = Systems::NAMESPACE.first + "#{system.id}/module/#{mod_id}" - result = curl( - method: "PUT", + result = client.put( path: path, headers: headers, ) @@ -24,8 +23,7 @@ module PlaceOS::Api path = Systems::NAMESPACE.first + "#{system.id}/module/#{mod_id}" - result = curl( - method: "DELETE", + result = client.delete( path: path, headers: headers, ) @@ -38,612 +36,593 @@ module PlaceOS::Api describe Systems do _, authorization_header = authentication - base = Systems::NAMESPACE[0] - with_server do - Specs.test_404(base, model_name: Model::ControlSystem.table_name, headers: authorization_header) + Specs.test_404(Systems.base_route, model_name: Model::ControlSystem.table_name, headers: authorization_header) - describe "index", tags: "search" do - Specs.test_base_index(klass: Model::ControlSystem, controller_klass: Systems) + describe "index", tags: "search" do + Specs.test_base_index(klass: Model::ControlSystem, controller_klass: Systems) - context "query parameter" do - it "zone_id filters systems by zones" do - Model::ControlSystem.clear + context "query parameter" do + it "zone_id filters systems by zones" do + Model::ControlSystem.clear - num_systems = 5 + num_systems = 5 - zone = Model::Generator.zone.save! - zone_id = zone.id.as(String) + zone = Model::Generator.zone.save! + zone_id = zone.id.as(String) - systems = Array.new(size: num_systems) do - Model::Generator.control_system - end + systems = Array.new(size: num_systems) do + Model::Generator.control_system + end - # Add the zone to a subset of systems - expected_systems = systems.shuffle[0..2] - expected_systems.each do |sys| - sys.zones = [zone_id] - end - systems.each &.save! + # Add the zone to a subset of systems + expected_systems = systems.shuffle[0..2] + expected_systems.each do |sys| + sys.zones = [zone_id] + end + systems.each &.save! - expected_ids = expected_systems.compact_map(&.id) - total_ids = expected_ids.size + expected_ids = expected_systems.compact_map(&.id) + total_ids = expected_ids.size - params = HTTP::Params.encode({"zone_id" => zone_id}) - path = "#{base}?#{params}" + params = HTTP::Params.encode({"zone_id" => zone_id}) + path = "#{Systems.base_route}?#{params}" - refresh_elastic(Model::ControlSystem.table_name) - found = until_expected("GET", path, authorization_header) do |response| - returned_ids = Array(Hash(String, JSON::Any)).from_json(response.body).map(&.["id"].as_s) - (returned_ids | expected_ids).size == total_ids - end - - found.should be_true + refresh_elastic(Model::ControlSystem.table_name) + found = until_expected("GET", path, authorization_header) do |response| + returned_ids = Array(Hash(String, JSON::Any)).from_json(response.body).map(&.["id"].as_s) + (returned_ids | expected_ids).size == total_ids end - it "email filters systems by email" do - Model::ControlSystem.clear - num_systems = 5 + found.should be_true + end - systems = Array.new(size: num_systems) do - Model::Generator.control_system - end + it "email filters systems by email" do + Model::ControlSystem.clear + num_systems = 5 - # Add the zone to a subset of systems - expected_systems = systems.shuffle[0..2] - systems.each &.save! + systems = Array.new(size: num_systems) do + Model::Generator.control_system + end - expected_emails = expected_systems.compact_map(&.email) - expected_ids = expected_systems.compact_map(&.id) + # Add the zone to a subset of systems + expected_systems = systems.shuffle[0..2] + systems.each &.save! - total_ids = expected_ids.size - params = HTTP::Params.encode({"email" => expected_emails.join(',')}) - path = "#{base}?#{params}" + expected_emails = expected_systems.compact_map(&.email) + expected_ids = expected_systems.compact_map(&.id) - found = until_expected("GET", path, authorization_header) do |response| - refresh_elastic(Model::ControlSystem.table_name) - returned_ids = Array(Hash(String, JSON::Any)).from_json(response.body).map(&.["id"].as_s) - (returned_ids | expected_ids).size == total_ids - end + total_ids = expected_ids.size + params = HTTP::Params.encode({"email" => expected_emails.join(',')}) + path = "#{Systems.base_route}?#{params}" - found.should be_true + found = until_expected("GET", path, authorization_header) do |response| + refresh_elastic(Model::ControlSystem.table_name) + returned_ids = Array(Hash(String, JSON::Any)).from_json(response.body).map(&.["id"].as_s) + (returned_ids | expected_ids).size == total_ids end - it "module_id filters systems by modules" do - Model::ControlSystem.clear - num_systems = 5 + found.should be_true + end - mod = Model::Generator.module.save! - module_id = mod.id.as(String) + it "module_id filters systems by modules" do + Model::ControlSystem.clear + num_systems = 5 - systems = Array.new(size: num_systems) do - Model::Generator.control_system - end + mod = Model::Generator.module.save! + module_id = mod.id.as(String) - # Add the zone to a subset of systems - expected_systems = systems.shuffle[0..2] - expected_systems.each do |sys| - sys.modules = [module_id] - end - systems.each &.save! + systems = Array.new(size: num_systems) do + Model::Generator.control_system + end - expected_ids = expected_systems.compact_map(&.id) - total_ids = expected_ids.size + # Add the zone to a subset of systems + expected_systems = systems.shuffle[0..2] + expected_systems.each do |sys| + sys.modules = [module_id] + end + systems.each &.save! - params = HTTP::Params.encode({"module_id" => module_id}) - path = "#{base}?#{params}" + expected_ids = expected_systems.compact_map(&.id) + total_ids = expected_ids.size - found = until_expected("GET", path, authorization_header) do |response| - refresh_elastic(Model::ControlSystem.table_name) - returned_ids = Array(Hash(String, JSON::Any)).from_json(response.body).map(&.["id"].as_s) - (returned_ids | expected_ids).size == total_ids - end + params = HTTP::Params.encode({"module_id" => module_id}) + path = "#{Systems.base_route}?#{params}" - found.should be_true + found = until_expected("GET", path, authorization_header) do |response| + refresh_elastic(Model::ControlSystem.table_name) + returned_ids = Array(Hash(String, JSON::Any)).from_json(response.body).map(&.["id"].as_s) + (returned_ids | expected_ids).size == total_ids end + + found.should be_true end end + end - describe "GET /systems/:sys_id/zones" do - it "lists zones for a system" do - control_system = Model::Generator.control_system.save! + describe "GET /systems/:sys_id/zones" do + it "lists zones for a system" do + control_system = Model::Generator.control_system.save! - zone0 = Model::Generator.zone.save! - zone1 = Model::Generator.zone.save! + zone0 = Model::Generator.zone.save! + zone1 = Model::Generator.zone.save! - control_system.zones = [zone0.id.as(String), zone1.id.as(String)] - control_system.save! + control_system.zones = [zone0.id.as(String), zone1.id.as(String)] + control_system.save! - path = base + "#{control_system.id}/zones" + path = Systems.base_route + "#{control_system.id}/zones" - result = curl( - method: "GET", - path: path, - headers: authorization_header, - ) + result = client.get( + path: path, + headers: authorization_header, + ) - result.status_code.should eq 200 - documents = Array(Hash(String, JSON::Any)).from_json(result.body) - documents.size.should eq 2 - documents.map(&.["id"].as_s).sort!.should eq [zone0.id, zone1.id].compact.sort! - end + result.status_code.should eq 200 + documents = Array(Hash(String, JSON::Any)).from_json(result.body) + documents.size.should eq 2 + documents.map(&.["id"].as_s).sort!.should eq [zone0.id, zone1.id].compact.sort! end + end - describe "PUT /systems/:sys_id/module/:module_id" do - it "adds a module if not present" do - cs = Model::Generator.control_system.save! - mod = Model::Generator.module.save! - cs.persisted?.should be_true - mod.persisted?.should be_true + describe "PUT /systems/:sys_id/module/:module_id" do + it "adds a module if not present" do + cs = Model::Generator.control_system.save! + mod = Model::Generator.module.save! + cs.persisted?.should be_true + mod.persisted?.should be_true - spec_add_module(cs, mod, authorization_header) - {cs, mod}.each &.destroy - end + spec_add_module(cs, mod, authorization_header) + {cs, mod}.each &.destroy + end - it "404s if added module does not exist" do - cs = Model::Generator.control_system.save! - cs.persisted?.should be_true + it "404s if added module does not exist" do + cs = Model::Generator.control_system.save! + cs.persisted?.should be_true - path = base + "#{cs.id}/module/mod-th15do35n073x157" + path = Systems.base_route + "#{cs.id}/module/mod-th15do35n073x157" - result = curl( - method: "PUT", - path: path, - headers: authorization_header, - ) + result = client.put( + path: path, + headers: authorization_header, + ) - result.status_code.should eq 404 - cs.destroy - end + result.status_code.should eq 404 + cs.destroy + end - it "adds module after removal from system" do - cs1 = Model::Generator.control_system.save! - cs2 = Model::Generator.control_system.save! + it "adds module after removal from system" do + cs1 = Model::Generator.control_system.save! + cs2 = Model::Generator.control_system.save! - mod = Model::Generator.module.save! + mod = Model::Generator.module.save! - cs1.persisted?.should be_true - cs2.persisted?.should be_true - mod.persisted?.should be_true + cs1.persisted?.should be_true + cs2.persisted?.should be_true + mod.persisted?.should be_true - cs1 = spec_add_module(cs1, mod, authorization_header) + cs1 = spec_add_module(cs1, mod, authorization_header) - spec_add_module(cs2, mod, authorization_header) + spec_add_module(cs2, mod, authorization_header) - cs1 = spec_delete_module(cs1, mod, authorization_header) + cs1 = spec_delete_module(cs1, mod, authorization_header) - spec_add_module(cs1, mod, authorization_header) - end + spec_add_module(cs1, mod, authorization_header) end + end - describe "DELETE /systems/:sys_id/module/:module_id" do - it "removes if not in use by another ControlSystem" do - cs = Model::Generator.control_system.save! - mod = Model::Generator.module(control_system: cs).save! - cs.persisted?.should be_true - mod.persisted?.should be_true + describe "DELETE /systems/:sys_id/module/:module_id" do + it "removes if not in use by another ControlSystem" do + cs = Model::Generator.control_system.save! + mod = Model::Generator.module(control_system: cs).save! + cs.persisted?.should be_true + mod.persisted?.should be_true - mod_id = mod.id.as(String) - cs_id = cs.id.as(String) + mod_id = mod.id.as(String) + cs_id = cs.id.as(String) - Model::ControlSystem.add_module(cs_id, mod_id) + Model::ControlSystem.add_module(cs_id, mod_id) - mod_id = mod.id.as(String) + mod_id = mod.id.as(String) - spec_delete_module(cs, mod, authorization_header) + spec_delete_module(cs, mod, authorization_header) - Model::Module.find(mod_id).should be_nil - {mod, cs}.each &.try &.destroy - end + Model::Module.find(mod_id).should be_nil + {mod, cs}.each &.try &.destroy + end - it "keeps module if in use by another ControlSystem" do - cs1 = Model::Generator.control_system.save! - cs2 = Model::Generator.control_system.save! - mod = Model::Generator.module.save! - cs1.persisted?.should be_true - cs2.persisted?.should be_true - mod.persisted?.should be_true + it "keeps module if in use by another ControlSystem" do + cs1 = Model::Generator.control_system.save! + cs2 = Model::Generator.control_system.save! + mod = Model::Generator.module.save! + cs1.persisted?.should be_true + cs2.persisted?.should be_true + mod.persisted?.should be_true - mod_id = mod.id.as(String) - # Add module to systems - cs1.update_fields(modules: [mod_id]) - cs2.update_fields(modules: [mod_id]) + mod_id = mod.id.as(String) + # Add module to systems + cs1.update_fields(modules: [mod_id]) + cs2.update_fields(modules: [mod_id]) - cs1.modules.should contain mod_id - cs2.modules.should contain mod_id + cs1.modules.should contain mod_id + cs2.modules.should contain mod_id - cs1 = spec_delete_module(cs1, mod, authorization_header) + cs1 = spec_delete_module(cs1, mod, authorization_header) - cs2 = Model::ControlSystem.find!(cs2.id.as(String)) - cs2.modules.should contain mod_id + cs2 = Model::ControlSystem.find!(cs2.id.as(String)) + cs2.modules.should contain mod_id - Model::Module.find(mod_id).should_not be_nil + Model::Module.find(mod_id).should_not be_nil - {mod, cs1, cs2}.each &.destroy - end + {mod, cs1, cs2}.each &.destroy end + end - describe "GET /systems/:sys_id/settings" do - it "collates System settings" do - control_system = Model::Generator.control_system.save! - control_system_settings_string = %(frangos: 1) - Model::Generator.settings(control_system: control_system, settings_string: control_system_settings_string).save! - - zone0 = Model::Generator.zone.save! - zone0_settings_string = %(screen: 1) - Model::Generator.settings(zone: zone0, settings_string: zone0_settings_string).save! - zone1 = Model::Generator.zone.save! - zone1_settings_string = %(meme: 2) - Model::Generator.settings(zone: zone1, settings_string: zone1_settings_string).save! - - control_system.zones = [zone0.id.as(String), zone1.id.as(String)] - control_system.update! - - expected_settings_ids = [ - control_system.settings, - zone1.settings, - zone0.settings, - ].flat_map(&.compact_map(&.id)).reverse! - - path = "#{base}#{control_system.id}/settings" - result = curl( - method: "GET", - path: path, - headers: authorization_header, - ) - - result.success?.should be_true + describe "GET /systems/:sys_id/settings" do + it "collates System settings" do + control_system = Model::Generator.control_system.save! + control_system_settings_string = %(frangos: 1) + Model::Generator.settings(control_system: control_system, settings_string: control_system_settings_string).save! + + zone0 = Model::Generator.zone.save! + zone0_settings_string = %(screen: 1) + Model::Generator.settings(zone: zone0, settings_string: zone0_settings_string).save! + zone1 = Model::Generator.zone.save! + zone1_settings_string = %(meme: 2) + Model::Generator.settings(zone: zone1, settings_string: zone1_settings_string).save! + + control_system.zones = [zone0.id.as(String), zone1.id.as(String)] + control_system.update! + + expected_settings_ids = [ + control_system.settings, + zone1.settings, + zone0.settings, + ].flat_map(&.compact_map(&.id)).reverse! + + path = "#{Systems.base_route}#{control_system.id}/settings" + result = client.get( + path: path, + headers: authorization_header, + ) - settings = Array(Hash(String, JSON::Any)).from_json(result.body) - settings_hierarchy_ids = settings.map &.["id"].to_s + result.success?.should be_true - settings_hierarchy_ids.should eq expected_settings_ids - {control_system, zone0, zone1}.each &.destroy - end + settings = Array(Hash(String, JSON::Any)).from_json(result.body) + settings_hierarchy_ids = settings.map &.["id"].to_s - it "returns an empty array for a system without associated settings" do - control_system = Model::Generator.control_system.save! + settings_hierarchy_ids.should eq expected_settings_ids + {control_system, zone0, zone1}.each &.destroy + end - zone0 = Model::Generator.zone.save! - zone1 = Model::Generator.zone.save! + it "returns an empty array for a system without associated settings" do + control_system = Model::Generator.control_system.save! - control_system.zones = [zone0.id.as(String), zone1.id.as(String)] - control_system.save! - path = "#{base}#{control_system.id}/settings" + zone0 = Model::Generator.zone.save! + zone1 = Model::Generator.zone.save! - result = curl( - method: "GET", - path: path, - headers: authorization_header, - ) + control_system.zones = [zone0.id.as(String), zone1.id.as(String)] + control_system.save! + path = "#{Systems.base_route}#{control_system.id}/settings" - unless result.success? - puts "\ncode: #{result.status_code} body: #{result.body}" - end + result = client.get( + path: path, + headers: authorization_header, + ) - result.success?.should be_true - Array(JSON::Any).from_json(result.body).should be_empty + unless result.success? + puts "\ncode: #{result.status_code} body: #{result.body}" end + + result.success?.should be_true + Array(JSON::Any).from_json(result.body).should be_empty end + end - it "GET /systems/:sys_id/functions/:module_slug" do - cs = PlaceOS::Model::Generator.control_system.save! - mod = PlaceOS::Model::Generator.module(control_system: cs).save! - module_slug = mod.id.as(String) + it "GET /systems/:sys_id/functions/:module_slug" do + cs = PlaceOS::Model::Generator.control_system.save! + mod = PlaceOS::Model::Generator.module(control_system: cs).save! + module_slug = mod.id.as(String) - sys_lookup = PlaceOS::Driver::RedisStorage.new(cs.id.as(String), "system") - lookup_key = "#{module_slug}/1" - sys_lookup[lookup_key] = module_slug + sys_lookup = PlaceOS::Driver::RedisStorage.new(cs.id.as(String), "system") + lookup_key = "#{module_slug}/1" + sys_lookup[lookup_key] = module_slug - PlaceOS::Driver::RedisStorage.with_redis do |redis| - meta = PlaceOS::Driver::DriverModel::Metadata.new({ - "function1" => {} of String => JSON::Any, - "function2" => {"arg1" => JSON.parse(%({"type":"integer"}))}, - "function3" => {"arg1" => JSON.parse(%({"type":"integer"})), "arg2" => JSON.parse(%({"type":"integer","default":200}))}, - }, ["Functoids"]) + PlaceOS::Driver::RedisStorage.with_redis do |redis| + meta = PlaceOS::Driver::DriverModel::Metadata.new({ + "function1" => {} of String => JSON::Any, + "function2" => {"arg1" => JSON.parse(%({"type":"integer"}))}, + "function3" => {"arg1" => JSON.parse(%({"type":"integer"})), "arg2" => JSON.parse(%({"type":"integer","default":200}))}, + }, ["Functoids"]) - redis.set("interface/#{module_slug}", meta.to_json) - end + redis.set("interface/#{module_slug}", meta.to_json) + end - path = base + "#{cs.id}/functions/#{module_slug}" + path = Systems.base_route + "#{cs.id}/functions/#{module_slug}" - result = curl( - method: "GET", - path: path, - headers: authorization_header, - ) + result = client.get( + path: path, + headers: authorization_header, + ) - result.body.includes?("function1").should be_true - end - - describe "GET /systems/:sys_id/types" do - it "returns types of modules in a system" do - expected = { - "Display" => 2, - "Switcher" => 1, - "Camera" => 3, - "Bookings" => 1, - } + result.body.includes?("function1").should be_true + end - cs = Model::Generator.control_system.save! - mods = expected.flat_map do |name, count| - Array(Model::Module).new(size: count) do - mod = Model::Generator.module - mod.custom_name = name - mod.save! - end + describe "GET /systems/:sys_id/types" do + it "returns types of modules in a system" do + expected = { + "Display" => 2, + "Switcher" => 1, + "Camera" => 3, + "Bookings" => 1, + } + + cs = Model::Generator.control_system.save! + mods = expected.flat_map do |name, count| + Array(Model::Module).new(size: count) do + mod = Model::Generator.module + mod.custom_name = name + mod.save! end + end - cs.modules = mods.compact_map(&.id) - cs.update! + cs.modules = mods.compact_map(&.id) + cs.update! - path = base + "#{cs.id}/types" + path = Systems.base_route + "#{cs.id}/types" - result = curl( - method: "GET", - path: path, - headers: authorization_header, - ) + result = client.get( + path: path, + headers: authorization_header, + ) - result.status_code.should eq 200 - types = Hash(String, Int32).from_json(result.body) + result.status_code.should eq 200 + types = Hash(String, Int32).from_json(result.body) - types.should eq expected + types.should eq expected - mods.each &.destroy - cs.destroy - end + mods.each &.destroy + cs.destroy end + end - context "with core" do - mod, cs = get_sys + context "with core" do + mod, cs = get_sys - # "fetches the state for `key` in module defined by `module_slug` - it "GET /systems/:sys_id/:module_slug/:key" do - module_slug = cs.modules.first + # "fetches the state for `key` in module defined by `module_slug` + it "GET /systems/:sys_id/:module_slug/:key" do + module_slug = cs.modules.first - # Create a storage proxy - driver_proxy = PlaceOS::Driver::RedisStorage.new mod.id.as(String) + # Create a storage proxy + driver_proxy = PlaceOS::Driver::RedisStorage.new mod.id.as(String) - status_name = "orange" - driver_proxy[status_name] = 1 + status_name = "orange" + driver_proxy[status_name] = 1 - sys_lookup = PlaceOS::Driver::RedisStorage.new(cs.id.as(String), "system") - lookup_key = "#{module_slug}/1" - sys_lookup[lookup_key] = mod.id.as(String) + sys_lookup = PlaceOS::Driver::RedisStorage.new(cs.id.as(String), "system") + lookup_key = "#{module_slug}/1" + sys_lookup[lookup_key] = mod.id.as(String) - path = base + "#{cs.id}/#{module_slug}/orange" + path = Systems.base_route + "#{cs.id}/#{module_slug}/orange" - response = curl( - method: "GET", - path: path, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) - String.from_json(response.body).should eq("1") - end + response = client.get( + path: path, + headers: authorization_header, + ) + String.from_json(response.body).should eq("1") + end - it "GET /systems/:sys_id/:module_slug" do - module_slug = cs.modules.first + it "GET /systems/:sys_id/:module_slug" do + module_slug = cs.modules.first - # Create a storage proxy - driver_proxy = PlaceOS::Driver::RedisStorage.new mod.id.as(String) + # Create a storage proxy + driver_proxy = PlaceOS::Driver::RedisStorage.new mod.id.as(String) - status_name = "nugget" - driver_proxy[status_name] = 1 + status_name = "nugget" + driver_proxy[status_name] = 1 - sys_lookup = PlaceOS::Driver::RedisStorage.new(cs.id.as(String), "system") - lookup_key = "#{module_slug}/1" - sys_lookup[lookup_key] = mod.id.as(String) + sys_lookup = PlaceOS::Driver::RedisStorage.new(cs.id.as(String), "system") + lookup_key = "#{module_slug}/1" + sys_lookup[lookup_key] = mod.id.as(String) - path = base + "#{cs.id}/#{module_slug}" + path = Systems.base_route + "#{cs.id}/#{module_slug}" - response = curl( - method: "GET", - path: path, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) + response = client.get( + path: path, + headers: authorization_header, + ) - state = Hash(String, String).from_json(response.body) - state["nugget"].should eq("1") - end + state = Hash(String, String).from_json(response.body) + state["nugget"].should eq("1") end + end - describe "POST /systems/:sys_id/start" do - it "start modules in a system" do - cs = Model::Generator.control_system.save! - mod = Model::Generator.module(control_system: cs).save! - cs.update_fields(modules: [mod.id.as(String)]) + describe "POST /systems/:sys_id/start" do + it "start modules in a system" do + cs = Model::Generator.control_system.save! + mod = Model::Generator.module(control_system: cs).save! + cs.update_fields(modules: [mod.id.as(String)]) - cs.persisted?.should be_true - mod.persisted?.should be_true - mod.running.should be_false + cs.persisted?.should be_true + mod.persisted?.should be_true + mod.running.should be_false - path = base + "#{cs.id}/start" + path = Systems.base_route + "#{cs.id}/start" - result = curl( - method: "POST", - path: path, - headers: authorization_header, - ) + result = client.post( + path: path, + headers: authorization_header, + ) - result.status_code.should eq 200 - Model::Module.find!(mod.id.as(String)).running.should be_true + result.status_code.should eq 200 + Model::Module.find!(mod.id.as(String)).running.should be_true - mod.destroy - cs.destroy - end + mod.destroy + cs.destroy end + end - describe "POST /systems/:sys_id/stop" do - it "stops modules in a system" do - cs = Model::Generator.control_system.save! - mod = Model::Generator.module(control_system: cs) - mod.running = true - mod.save! - cs.update_fields(modules: [mod.id.as(String)]) + describe "POST /systems/:sys_id/stop" do + it "stops modules in a system" do + cs = Model::Generator.control_system.save! + mod = Model::Generator.module(control_system: cs) + mod.running = true + mod.save! + cs.update_fields(modules: [mod.id.as(String)]) - cs.persisted?.should be_true - mod.persisted?.should be_true - mod.running.should be_true + cs.persisted?.should be_true + mod.persisted?.should be_true + mod.running.should be_true - path = base + "#{cs.id}/stop" + path = Systems.base_route + "#{cs.id}/stop" - result = curl( - method: "POST", - path: path, - headers: authorization_header.merge({"Content-Type" => "application/x-www-form-urlencoded"}), - ) + result = client.post( + path: path, + headers: authorization_header, + ) - result.status_code.should eq 200 - Model::Module.find!(mod.id.as(String)).running.should be_false + result.status_code.should eq 200 + Model::Module.find!(mod.id.as(String)).running.should be_false - mod.destroy - cs.destroy - end + mod.destroy + cs.destroy end + end - describe "GET /systems/:sys_id/metadata" do - it "shows system metadata" do - system = Model::Generator.control_system.save! - system_id = system.id.as(String) - meta = Model::Generator.metadata(name: "special", parent: system_id).save! + describe "GET /systems/:sys_id/metadata" do + it "shows system metadata" do + system = Model::Generator.control_system.save! + system_id = system.id.as(String) + meta = Model::Generator.metadata(name: "special", parent: system_id).save! - result = curl( - method: "GET", - path: base + "#{system_id}/metadata", - headers: authorization_header, - ) + result = client.get( + path: Systems.base_route + "#{system_id}/metadata", + headers: authorization_header, + ) - metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) - metadata.size.should eq 1 - metadata.first[1].parent_id.should eq system_id - metadata.first[1].name.should eq meta.name + metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) + metadata.size.should eq 1 + metadata.first[1].parent_id.should eq system_id + metadata.first[1].name.should eq meta.name - system.destroy - meta.destroy - end + system.destroy + meta.destroy end + end - describe "CRUD operations", tags: "crud" do - Specs.test_crd(klass: Model::ControlSystem, controller_klass: Systems) + describe "CRUD operations", tags: "crud" do + Specs.test_crd(klass: Model::ControlSystem, controller_klass: Systems) - describe "update" do - it "if version is valid" do - cs = Model::Generator.control_system.save! - cs.persisted?.should be_true + describe "update" do + it "if version is valid" do + cs = Model::Generator.control_system.save! + cs.persisted?.should be_true - original_name = cs.name - cs.name = random_name + original_name = cs.name + cs.name = random_name - id = cs.id.as(String) + id = cs.id.as(String) - params = HTTP::Params.encode({"version" => "0"}) - path = "#{File.join(base, id)}?#{params}" + params = HTTP::Params.encode({"version" => "0"}) + path = "#{File.join(Systems.base_route, id)}?#{params}" - result = curl( - method: "PATCH", - path: path, - body: cs.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) + result = client.patch( + path: path, + body: cs.to_json, + headers: authorization_header, + ) - result.status_code.should eq 200 - updated = Model::ControlSystem.from_trusted_json(result.body) - updated.id.should eq cs.id - updated.name.should_not eq original_name - end + result.status_code.should eq 200 + updated = Model::ControlSystem.from_trusted_json(result.body) + updated.id.should eq cs.id + updated.name.should_not eq original_name + end - it "fails when version is invalid" do - cs = Model::Generator.control_system.save! - id = cs.id.as(String) - cs.persisted?.should be_true + it "fails when version is invalid" do + cs = Model::Generator.control_system.save! + id = cs.id.as(String) + cs.persisted?.should be_true - params = HTTP::Params.encode({"version" => "2"}) - path = "#{File.join(base, id)}?#{params}" + params = HTTP::Params.encode({"version" => "2"}) + path = "#{File.join(Systems.base_route, id)}?#{params}" - result = curl( - method: "PATCH", - path: path, - body: cs.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) + result = client.patch( + path: path, + body: cs.to_json, + headers: authorization_header, + ) - result.status_code.should eq 409 - end + result.status_code.should eq 409 end end + end - describe "GET /systems/:id/metadata" do - it "shows system metadata" do - system = Model::Generator.control_system.save! - system_id = system.id.as(String) - meta = Model::Generator.metadata(name: "special", parent: system_id).save! + describe "GET /systems/:id/metadata" do + it "shows system metadata" do + system = Model::Generator.control_system.save! + system_id = system.id.as(String) + meta = Model::Generator.metadata(name: "special", parent: system_id).save! - result = curl( - method: "GET", - path: base + "#{system_id}/metadata", - headers: authorization_header, - ) + result = client.get( + path: Systems.base_route + "#{system_id}/metadata", + headers: authorization_header, + ) - metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) - metadata.size.should eq 1 - metadata.first[1].parent_id.should eq system_id - metadata.first[1].name.should eq meta.name + metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) + metadata.size.should eq 1 + metadata.first[1].parent_id.should eq system_id + metadata.first[1].name.should eq meta.name - system.destroy - meta.destroy - end + system.destroy + meta.destroy end + end - describe "scopes" do - Specs.test_controller_scope(Systems) - it "should not allow start" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("systems", :read)]) + describe "scopes" do + Specs.test_controller_scope(Systems) + it "should not allow start" do + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("systems", :read)]) - cs = Model::Generator.control_system.save! - mod = Model::Generator.module(control_system: cs).save! - cs.update_fields(modules: [mod.id.as(String)]) + cs = Model::Generator.control_system.save! + mod = Model::Generator.module(control_system: cs).save! + cs.update_fields(modules: [mod.id.as(String)]) - cs.persisted?.should be_true - mod.persisted?.should be_true - mod.running.should be_false + cs.persisted?.should be_true + mod.persisted?.should be_true + mod.running.should be_false - path = base + "#{cs.id}/start" + path = Systems.base_route + "#{cs.id}/start" - result = curl( - method: "POST", - path: path, - headers: scoped_authorization_header, - ) + result = client.post( + path: path, + headers: scoped_authorization_header, + ) - result.status_code.should eq 403 - end + result.status_code.should eq 403 + end - it "should allow start" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("systems", :write)]) + it "should allow start" do + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("systems", :write)]) - cs = Model::Generator.control_system.save! - mod = Model::Generator.module(control_system: cs).save! - cs.update_fields(modules: [mod.id.as(String)]) + cs = Model::Generator.control_system.save! + mod = Model::Generator.module(control_system: cs).save! + cs.update_fields(modules: [mod.id.as(String)]) - cs.persisted?.should be_true - mod.persisted?.should be_true - mod.running.should be_false + cs.persisted?.should be_true + mod.persisted?.should be_true + mod.running.should be_false - path = base + "#{cs.id}/start" + path = Systems.base_route + "#{cs.id}/start" - result = curl( - method: "POST", - path: path, - headers: scoped_authorization_header, - ) + result = client.post( + path: path, + headers: scoped_authorization_header, + ) - result.status_code.should eq 200 - Model::Module.find!(mod.id.as(String)).running.should be_true + result.status_code.should eq 200 + Model::Module.find!(mod.id.as(String)).running.should be_true - mod.destroy - cs.destroy - end + mod.destroy + cs.destroy end end end diff --git a/spec/controllers/triggers_spec.cr b/spec/controllers/triggers_spec.cr index acec98fb..b8f80cab 100644 --- a/spec/controllers/triggers_spec.cr +++ b/spec/controllers/triggers_spec.cr @@ -3,84 +3,78 @@ require "../helper" module PlaceOS::Api describe Triggers do _authenticated_user, authorization_header = authentication - base = Triggers::NAMESPACE[0] - with_server do - Specs.test_404(base, model_name: Model::Trigger.table_name, headers: authorization_header) + Specs.test_404(Triggers.base_route, model_name: Model::Trigger.table_name, headers: authorization_header) - describe "index", tags: "search" do - Specs.test_base_index(klass: Model::Trigger, controller_klass: Triggers) - end + describe "index", tags: "search" do + Specs.test_base_index(klass: Model::Trigger, controller_klass: Triggers) + end - describe "GET /triggers/:id/instances" do - it "lists instances for a Trigger" do - trigger = Model::Generator.trigger.save! - instances = Array(Model::TriggerInstance).new(size: 3) { Model::Generator.trigger_instance(trigger).save! } + describe "GET /triggers/:id/instances" do + it "lists instances for a Trigger" do + trigger = Model::Generator.trigger.save! + instances = Array(Model::TriggerInstance).new(size: 3) { Model::Generator.trigger_instance(trigger).save! } - response = curl( - method: "GET", - path: File.join(base, trigger.id.not_nil!, "instances"), - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) + response = client.get( + path: File.join(Triggers.base_route, trigger.id.not_nil!, "instances"), + headers: authorization_header, + ) - # Can't use from_json directly on the model as `id` will not be parsed - result = Array(JSON::Any).from_json(response.body).map { |d| Model::TriggerInstance.from_trusted_json(d.to_json) } + # Can't use from_json directly on the model as `id` will not be parsed + result = Array(JSON::Any).from_json(response.body).map { |d| Model::TriggerInstance.from_trusted_json(d.to_json) } - result.all? { |i| i.trigger_id == trigger.id }.should be_true - instances.compact_map(&.id).sort!.should eq result.compact_map(&.id).sort! - end + result.all? { |i| i.trigger_id == trigger.id }.should be_true + instances.compact_map(&.id).sort!.should eq result.compact_map(&.id).sort! end + end - describe "CRUD operations", tags: "crud" do - Specs.test_crd(klass: Model::Trigger, controller_klass: Triggers) - it "update" do - trigger = Model::Generator.trigger.save! - original_name = trigger.name + describe "CRUD operations", tags: "crud" do + Specs.test_crd(klass: Model::Trigger, controller_klass: Triggers) + it "update" do + trigger = Model::Generator.trigger.save! + original_name = trigger.name - trigger.name = random_name + trigger.name = random_name - id = trigger.id.as(String) - path = File.join(base, id) - result = curl( - method: "PATCH", - path: path, - body: trigger.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) + id = trigger.id.as(String) + path = File.join(Triggers.base_route, id) + result = client.patch( + path: path, + body: trigger.to_json, + headers: authorization_header, + ) - result.status_code.should eq 200 - updated = Model::Trigger.from_trusted_json(result.body) + result.status_code.should eq 200 + updated = Model::Trigger.from_trusted_json(result.body) - updated.id.should eq trigger.id - updated.name.should_not eq original_name - updated.destroy - end + updated.id.should eq trigger.id + updated.name.should_not eq original_name + updated.destroy + end - describe "show" do - it "includes trigger_instances with truthy `instances`" do - trigger = Model::Generator.trigger.save! - trigger_instance = Model::Generator.trigger_instance(trigger).save! - trigger_instance_id = trigger_instance.id.as(String) + describe "show" do + it "includes trigger_instances with truthy `instances`" do + trigger = Model::Generator.trigger.save! + trigger_instance = Model::Generator.trigger_instance(trigger).save! + trigger_instance_id = trigger_instance.id.as(String) - params = HTTP::Params{"instances" => "true"} - path = "#{base}#{trigger.id}?#{params}" + params = HTTP::Params{"instances" => "true"} + path = "#{Triggers.base_route}#{trigger.id}?#{params}" - result = curl( - method: "GET", - path: path, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) + result = client.get( + path: path, + headers: authorization_header, + ) - response = JSON.parse(result.body) - response["trigger_instances"].as_a?.try &.first?.try &.["id"].to_s.should eq trigger_instance_id - end + response = JSON.parse(result.body) + response["trigger_instances"].as_a?.try &.first?.try &.["id"].to_s.should eq trigger_instance_id end end end + end - describe "scopes" do - Specs.test_controller_scope(Triggers) - Specs.test_update_write_scope(Triggers) - end + describe "scopes" do + Specs.test_controller_scope(Triggers) + Specs.test_update_write_scope(Triggers) end end diff --git a/spec/controllers/users_spec.cr b/spec/controllers/users_spec.cr index 5751f0be..12c77775 100644 --- a/spec/controllers/users_spec.cr +++ b/spec/controllers/users_spec.cr @@ -3,178 +3,166 @@ require "../helper" module PlaceOS::Api describe Users do authenticated_user, authorization_header = authentication - base = Users::NAMESPACE[0] + Specs.test_404(Users.base_route, model_name: Model::User.table_name, headers: authorization_header) - with_server do - Specs.test_404(base, model_name: Model::User.table_name, headers: authorization_header) + describe "CRUD operations", tags: "crud" do + it "query via email" do + model = Model::Generator.user.save! + model.persisted?.should be_true + id = model.id.as(String) - describe "CRUD operations", tags: "crud" do - it "query via email" do - model = Model::Generator.user.save! - model.persisted?.should be_true - id = model.id.as(String) + params = HTTP::Params.encode({"q" => model.email.to_s}) + path = "#{Users.base_route}?#{params}" - params = HTTP::Params.encode({"q" => model.email.to_s}) - path = "#{base}?#{params}" + sleep 2 - sleep 2 + result = client.get( + path: path, + headers: authorization_header, + ) - result = curl( - method: "GET", - path: path, - headers: authorization_header, - ) + result.status_code.should eq 200 + response = Array(Model::User).from_json(result.body) + response.size.should eq 1 + response.first.id.should eq id + end - result.status_code.should eq 200 - response = Array(Model::User).from_json(result.body) - response.size.should eq 1 - response.first.id.should eq id - end + it "show" do + model = Model::Generator.user.save! + model.persisted?.should be_true + id = model.id.as(String) + result = client.get( + path: File.join(Users.base_route, id), + headers: authorization_header, + ) - it "show" do - model = Model::Generator.user.save! - model.persisted?.should be_true - id = model.id.as(String) - result = curl( - method: "GET", - path: File.join(base, id), - headers: authorization_header, - ) + result.status_code.should eq 200 + response_model = Model::User.from_trusted_json(result.body) + response_model.id.should eq id - result.status_code.should eq 200 - response_model = Model::User.from_trusted_json(result.body) - response_model.id.should eq id + model.destroy + end - model.destroy - end + it "show via email" do + model = Model::Generator.user.save! + model.persisted?.should be_true + id = model.id.as(String) + result = client.get( + path: Users.base_route + model.email.to_s, + headers: authorization_header, + ) + + result.status_code.should eq 200 + response_model = Model::User.from_trusted_json(result.body) + response_model.id.should eq id + response_model.email.should eq model.email + + model.destroy + end - it "show via email" do - model = Model::Generator.user.save! - model.persisted?.should be_true - id = model.id.as(String) - result = curl( - method: "GET", - path: base + model.email.to_s, - headers: authorization_header, - ) + it "show via login_name" do + login = random_name + model = Model::Generator.user + model.login_name = login + model.save! + model.persisted?.should be_true + id = model.id.as(String) + result = client.get( + path: Users.base_route + login, + headers: authorization_header, + ) + + result.status_code.should eq 200 + response_model = Model::User.from_trusted_json(result.body) + response_model.id.should eq id + + model.destroy + end - result.status_code.should eq 200 - response_model = Model::User.from_trusted_json(result.body) - response_model.id.should eq id - response_model.email.should eq model.email + it "show via staff_id" do + staff_id = "12345678" + model = Model::Generator.user + model.staff_id = staff_id + model.save! + model.persisted?.should be_true + id = model.id.as(String) + result = client.get( + path: Users.base_route + staff_id, + headers: authorization_header, + ) + + result.status_code.should eq 200 + response_model = Model::User.from_trusted_json(result.body) + response_model.id.should eq id + + model.destroy + end - model.destroy - end + describe "update" do + it "updates groups" do + initial_groups = ["public"] - it "show via login_name" do - login = random_name model = Model::Generator.user - model.login_name = login + model.groups = initial_groups model.save! model.persisted?.should be_true - id = model.id.as(String) - result = curl( - method: "GET", - path: base + login, - headers: authorization_header, - ) - result.status_code.should eq 200 - response_model = Model::User.from_trusted_json(result.body) - response_model.id.should eq id - - model.destroy - end + updated_groups = ["admin", "public", "calendar"] - it "show via staff_id" do - staff_id = "12345678" - model = Model::Generator.user - model.staff_id = staff_id - model.save! - model.persisted?.should be_true id = model.id.as(String) - result = curl( - method: "GET", - path: base + staff_id, - headers: authorization_header, + result = client.patch( + path: File.join(Users.base_route, id), + body: {groups: updated_groups}.to_json, + headers: authorization_header ) result.status_code.should eq 200 response_model = Model::User.from_trusted_json(result.body) response_model.id.should eq id + response_model.groups.should eq updated_groups model.destroy end - - describe "update" do - it "updates groups" do - initial_groups = ["public"] - - model = Model::Generator.user - model.groups = initial_groups - model.save! - model.persisted?.should be_true - - updated_groups = ["admin", "public", "calendar"] - - id = model.id.as(String) - result = curl( - method: "PATCH", - path: File.join(base, id), - body: {groups: updated_groups}.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}) - ) - - result.status_code.should eq 200 - response_model = Model::User.from_trusted_json(result.body) - response_model.id.should eq id - response_model.groups.should eq updated_groups - - model.destroy - end - end end + end - describe "GET /users/current" do - it "renders the current user" do - authenticated_user, authorization_header = authentication - result = curl( - method: "GET", - path: File.join(base, "/current"), - headers: authorization_header, - ) - - result.status_code.should eq 200 - response_user = Model::User.from_trusted_json(result.body) - response_user.id.should eq authenticated_user.id - end + describe "GET /users/current" do + it "renders the current user" do + authenticated_user, authorization_header = authentication + result = client.get( + path: File.join(Users.base_route, "/current"), + headers: authorization_header, + ) + + result.status_code.should eq 200 + response_user = Model::User.from_trusted_json(result.body) + response_user.id.should eq authenticated_user.id end + end - describe "GET /users/:id/metadata" do - it "shows user metadata" do - user = Model::Generator.user.save! - user_id = user.id.as(String) - meta = Model::Generator.metadata(name: "special", parent: user_id).save! + describe "GET /users/:id/metadata" do + it "shows user metadata" do + user = Model::Generator.user.save! + user_id = user.id.as(String) + meta = Model::Generator.metadata(name: "special", parent: user_id).save! - result = curl( - method: "GET", - path: base + "#{user_id}/metadata", - headers: authorization_header, - ) + result = client.get( + path: Users.base_route + "#{user_id}/metadata", + headers: authorization_header, + ) - metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) - metadata.size.should eq 1 - metadata.first[1].parent_id.should eq user_id - metadata.first[1].name.should eq meta.name + metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) + metadata.size.should eq 1 + metadata.first[1].parent_id.should eq user_id + metadata.first[1].name.should eq meta.name - user.destroy - meta.destroy - end + user.destroy + meta.destroy end + end - describe "scopes" do - Specs.test_controller_scope(Users) - end + describe "scopes" do + Specs.test_controller_scope(Users) end end end diff --git a/spec/controllers/zones_spec.cr b/spec/controllers/zones_spec.cr index c4063274..d534a6ea 100644 --- a/spec/controllers/zones_spec.cr +++ b/spec/controllers/zones_spec.cr @@ -3,66 +3,61 @@ require "../helper" module PlaceOS::Api describe Zones do _authenticated_user, authorization_header = authentication - base = Zones::NAMESPACE[0] - with_server do - Specs.test_404(base, model_name: Model::Zone.table_name, headers: authorization_header) + Specs.test_404(Zones.base_route, model_name: Model::Zone.table_name, headers: authorization_header) - describe "index", tags: "search" do - Specs.test_base_index(klass: Model::Zone, controller_klass: Zones) - end - - describe "CRUD operations", tags: "crud" do - Specs.test_crd(klass: Model::Zone, controller_klass: Zones) - it "update" do - zone = Model::Generator.zone.save! - original_name = zone.name - zone.name = random_name - - id = zone.id.as(String) - path = File.join(base, id) - result = curl( - method: "PATCH", - path: path, - body: zone.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) - - result.success?.should be_true - updated = Model::Zone.from_trusted_json(result.body) + describe "index", tags: "search" do + Specs.test_base_index(klass: Model::Zone, controller_klass: Zones) + end - updated.id.should eq zone.id - updated.name.should_not eq original_name - updated.destroy - end + describe "CRUD operations", tags: "crud" do + Specs.test_crd(klass: Model::Zone, controller_klass: Zones) + it "update" do + zone = Model::Generator.zone.save! + original_name = zone.name + zone.name = random_name + + id = zone.id.as(String) + path = File.join(Zones.base_route, id) + result = client.patch( + path: path, + body: zone.to_json, + headers: authorization_header, + ) + + result.success?.should be_true + updated = Model::Zone.from_trusted_json(result.body) + + updated.id.should eq zone.id + updated.name.should_not eq original_name + updated.destroy end + end - describe "GET /zones/:id/metadata" do - it "shows zone metadata" do - zone = Model::Generator.zone.save! - zone_id = zone.id.as(String) - meta = Model::Generator.metadata(name: "special", parent: zone_id).save! + describe "GET /zones/:id/metadata" do + it "shows zone metadata" do + zone = Model::Generator.zone.save! + zone_id = zone.id.as(String) + meta = Model::Generator.metadata(name: "special", parent: zone_id).save! - result = curl( - method: "GET", - path: base + "#{zone_id}/metadata", - headers: authorization_header, - ) + result = client.get( + path: Zones.base_route + "#{zone_id}/metadata", + headers: authorization_header, + ) - metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) - metadata.size.should eq 1 - metadata.first[1].parent_id.should eq zone_id - metadata.first[1].name.should eq meta.name + metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) + metadata.size.should eq 1 + metadata.first[1].parent_id.should eq zone_id + metadata.first[1].name.should eq meta.name - zone.destroy - meta.destroy - end + zone.destroy + meta.destroy end + end - describe "scopes" do - Specs.test_controller_scope(Zones) - Specs.test_update_write_scope(Zones) - end + describe "scopes" do + Specs.test_controller_scope(Zones) + Specs.test_update_write_scope(Zones) end end end diff --git a/spec/helper.cr b/spec/helper.cr index c2b3dce4..d4b7c74b 100644 --- a/spec/helper.cr +++ b/spec/helper.cr @@ -1,16 +1,15 @@ +require "action-controller/spec_helper" require "http" require "mutex" require "promise" require "random" require "rethinkdb-orm" require "simple_retry" +require "spec" -# Helper methods for testing controllers (curl, with_server, context) -require "../lib/action-controller/spec/curl_context" +require "./spec_helpers/*" -require "./spec_constants" -require "./scope_helper" -require "./http_mocks" +include PlaceOS::Api::SpecClient Spec.before_suite do Log.builder.bind("*", backend: PlaceOS::LogBackend::STDOUT, level: :trace) @@ -28,7 +27,6 @@ require "../src/config" # Generators for Engine models require "placeos-models/spec/generator" -require "spec" # Configure DB db_name = "place_#{ENV["SG_ENV"]? || "development"}" @@ -69,8 +67,9 @@ def x_api_authentication(sys_admin : Bool = true, support : Bool = true, scope = api_key.save! end - authorization_header = { - "X-API-Key" => api_key.x_api_key.not_nil!, + authorization_header = HTTP::Headers{ + "X-API-Key" => api_key.x_api_key.not_nil!, + "Content-Type" => "application/json", } {api_key.user, authorization_header} @@ -92,9 +91,12 @@ def authentication(sys_admin : Bool = true, support : Bool = true, scope = [Plac user.support = support user.save! end - authorization_header = { + + authorization_header = HTTP::Headers{ "Authorization" => "Bearer #{PlaceOS::Model::Generator.jwt(authenticated_user, scope).encode}", + "Content-Type" => "application/json", } + {authenticated_user, authorization_header} end end @@ -112,13 +114,14 @@ def generate_auth_user(sys_admin, support, scopes) end end -def until_expected(method, path, headers, timeout : Time::Span = 3.seconds, &block : HTTP::Client::Response -> Bool) +def until_expected(method, path, headers : HTTP::Headers, timeout : Time::Span = 3.seconds, &block : HTTP::Client::Response -> Bool) + client = ActionController::SpecHelper.client channel = Channel(Bool).new spawn do before = Time.utc begin SimpleRetry.try_to(base_interval: 50.milliseconds, max_elapsed_time: 2.seconds, retry_on: Exception) do - result = curl(method: method, path: path, headers: headers) + result = client.exec(method: method, path: path, headers: headers) unless result.success? puts "\nrequest failed with: #{result.status_code}" @@ -158,285 +161,3 @@ def refresh_elastic(index : String? = nil) path = "/#{index}" + path unless index.nil? Neuroplastic::Client.new.perform_request("POST", path) end - -module PlaceOS::Api::Specs - # Check application responds with 404 when model not present - def self.test_404(base, model_name, headers) - it "404s if #{model_name} isn't present in database", tags: "search" do - id = "#{model_name}-#{Random.rand(9999).to_s.ljust(4, '0')}" - path = File.join(base, id) - result = curl("GET", path: path, headers: headers) - result.status_code.should eq 404 - end - end - - # Test search on name field - macro test_base_index(klass, controller_klass) - {% klass_name = klass.stringify.split("::").last.underscore %} - - it "queries #{ {{ klass_name }} }", tags: "search" do - _, authorization_header = authentication - doc = PlaceOS::Model::Generator.{{ klass_name.id }} - name = random_name - doc.name = name - doc.save! - - refresh_elastic({{klass}}.table_name) - - doc.persisted?.should be_true - params = HTTP::Params.encode({"q" => name}) - path = "#{{{controller_klass}}::NAMESPACE[0].rstrip('/')}?#{params}" - header = authorization_header - - found = until_expected("GET", path, header) do |response| - Array(Hash(String, JSON::Any)) - .from_json(response.body) - .map(&.["id"].as_s) - .any?(doc.id) - end - found.should be_true - end - end - - macro test_create(klass, controller_klass) - {% klass_name = klass.stringify.split("::").last.underscore %} - base = {{ controller_klass }}::NAMESPACE[0] - - it "create" do - _, authorization_header = authentication - body = PlaceOS::Model::Generator.{{ klass_name.id }}.to_json - result = curl( - method: "POST", - path: base, - body: body, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) - - result.status_code.should eq 201 - response_model = {{ klass.id }}.from_trusted_json(result.body) - response_model.destroy - end - end - - macro test_show(klass, controller_klass) - {% klass_name = klass.stringify.split("::").last.underscore %} - base = {{ controller_klass }}::NAMESPACE[0] - - it "show" do - _, authorization_header = authentication - model = PlaceOS::Model::Generator.{{ klass_name.id }}.save! - model.persisted?.should be_true - id = model.id.as(String) - result = curl( - method: "GET", - path: File.join(base, id), - headers: authorization_header, - ) - - result.status_code.should eq 200 - response_model = {{ klass.id }}.from_trusted_json(result.body) - response_model.id.should eq id - - model.destroy - end - end - - macro test_destroy(klass, controller_klass) - {% klass_name = klass.stringify.split("::").last.underscore %} - base = {{ controller_klass }}::NAMESPACE[0] - - it "destroy" do - _, authorization_header = authentication - model = PlaceOS::Model::Generator.{{ klass_name.id }}.save! - model.persisted?.should be_true - id = model.id.as(String) - result = curl( - method: "DELETE", - path: File.join(base, id), - headers: authorization_header, - ) - - result.status_code.should eq 200 - {{ klass.id }}.find(id).should be_nil - end - end - - macro test_crd(klass, controller_klass) - Specs.test_create({{ klass }}, {{ controller_klass }}) - Specs.test_show({{ klass }}, {{ controller_klass }}) - Specs.test_destroy({{ klass }}, {{ controller_klass }}) - end - - macro test_controller_scope(klass) - {% base = klass.resolve.constant(:NAMESPACE).first %} - - {% if klass.stringify == "Repositories" %} - {% model_name = "Repository" %} - {% model_gen = "repository" %} - {% elsif klass.stringify == "Systems" %} - {% model_name = "ControlSystem" %} - {% model_gen = "control_system" %} - {% elsif klass.stringify == "Settings" %} - {% model_name = "Settings" %} - {% model_gen = "settings" %} - {% else %} - {% model_name = klass.stringify.gsub(/[s]$/, " ").strip %} - {% model_gen = model_name.underscore %} - {% end %} - - {% scope_name = klass.stringify.underscore %} - - context "read" do - it "allows access to show" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :read)]) - - model = PlaceOS::Model::Generator.{{ model_gen.id }}.save! - model.persisted?.should be_true - id = model.id.as(String) - result = show_route({{ base }}, id, scoped_authorization_header) - result.status_code.should eq 200 - response_model = Model::{{ model_name.id }}.from_trusted_json(result.body) - response_model.id.should eq id - model.destroy - end - - it "allows access to index" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :read)]) - - result = index_route({{ base }}, scoped_authorization_header) - result.success?.should be_true - end - - it "should not allow access to create" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :read)]) - - body = PlaceOS::Model::Generator.{{ model_gen.id }}.to_json - result = create_route({{ base }}, body, scoped_authorization_header) - result.status_code.should eq 403 - end - - it "should not allow access to delete" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :read)]) - - model = PlaceOS::Model::Generator.{{ model_gen.id }}.save! - model.persisted?.should be_true - id = model.id.as(String) - result = delete_route({{ base }}, id, scoped_authorization_header) - result.status_code.should eq 403 - Model::{{ model_name.id }}.find(id).should_not be_nil - end - end - - context "write" do - it "should not allow access to show" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :write)]) - - model = PlaceOS::Model::Generator.{{ model_gen.id }}.save! - model.persisted?.should be_true - id = model.id.as(String) - result = show_route({{ base }}, id, scoped_authorization_header) - result.status_code.should eq 403 - model.destroy - end - - it "should not allow access to index" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :write)]) - - result = index_route({{ base }}, scoped_authorization_header) - result.status_code.should eq 403 - end - - it "should allow access to create" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :write)]) - - body = PlaceOS::Model::Generator.{{ model_gen.id }}.to_json - result = create_route({{ base }}, body, scoped_authorization_header) - result.success?.should be_true - - response_model = Model::{{ model_name.id }}.from_trusted_json(result.body) - response_model.destroy - end - - it "should allow access to delete" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :write)]) - model = PlaceOS::Model::Generator.{{ model_gen.id }}.save! - model.persisted?.should be_true - id = model.id.as(String) - result = delete_route({{ base }}, id, scoped_authorization_header) - result.success?.should be_true - Model::{{ model_name.id }}.find(id).should be_nil - end - end - end - - macro test_update_write_scope(klass) - {% base = klass.resolve.constant(:NAMESPACE).first %} - - {% if klass.stringify == "Repositories" %} - {% model_name = "Repository" %} - {% model_gen = "repository" %} - {% else %} - {% model_name = klass.stringify.gsub(/[s]$/, " ").strip %} - {% model_gen = model_name.underscore %} - {% end %} - - {% scope_name = klass.stringify.underscore %} - - it "checks scope on update" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, PlaceOS::Model::UserJWT::Scope::Access::Write)]) - model = Model::Generator.{{ model_gen.id }}.save! - original_name = model.name - model.name = random_name - - id = model.id.as(String) - path = File.join({{ base }}, id) - result = update_route(path, model, scoped_authorization_header) - - result.success?.should be_true - updated = Model::{{ model_name.id }}.from_trusted_json(result.body) - - updated.id.should eq model.id - updated.name.should_not eq original_name - updated.destroy - - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, PlaceOS::Model::UserJWT::Scope::Access::Read)]) - result = update_route(path, model, scoped_authorization_header) - - result.success?.should be_false - result.status_code.should eq 403 - end - end -end - -class MockServer - include ActionController::Router - - def initialize - init_routes - end - - private def init_routes - {% for klass in ActionController::Base::CONCRETE_CONTROLLERS %} - {{klass}}.__init_routes__(self) - {% end %} - end -end - -MOCK_SERVER = MockServer.new - -abstract class PlaceOS::Api::Application < ActionController::Base - def self.with_request(verb, path, expect_failure = false, route_params = nil) - io = IO::Memory.new - context = context(verb.upcase, path) - - context.route_params = route_params if route_params - - MOCK_SERVER.route_handler.search_route(verb, path, "#{verb.downcase}#{path}", context) - context.response.output = io - - yield new(context) - - context.response.status.success?.should(expect_failure ? be_false : be_true) - context - end -end diff --git a/spec/scope_helper.cr b/spec/scope_helper.cr deleted file mode 100644 index 82d5a72b..00000000 --- a/spec/scope_helper.cr +++ /dev/null @@ -1,43 +0,0 @@ -require "../lib/action-controller/spec/curl_context" - -def show_route(base, id, scoped_authorization_header) - curl( - method: "GET", - path: File.join(base, id), - headers: scoped_authorization_header, - ) -end - -def index_route(base, scoped_authorization_header) - curl( - method: "GET", - path: base, - headers: scoped_authorization_header.merge({"Content-Type" => "application/json"}), - ) -end - -def create_route(base, body, scoped_authorization_header) - curl( - method: "POST", - path: base, - body: body, - headers: scoped_authorization_header.merge({"Content-Type" => "application/json"}), - ) -end - -def delete_route(base, id, scoped_authorization_header) - curl( - method: "DELETE", - path: File.join(base, id), - headers: scoped_authorization_header, - ) -end - -def update_route(path, body, scoped_authorization_header) - curl( - method: "PATCH", - path: path, - body: body.to_json, - headers: scoped_authorization_header.merge({"Content-Type" => "application/json"}), - ) -end diff --git a/spec/spec_constants.cr b/spec/spec_constants.cr deleted file mode 100644 index 2dfadcf1..00000000 --- a/spec/spec_constants.cr +++ /dev/null @@ -1,8 +0,0 @@ -require "./helper" - -module PlaceOS::Api - class_getter authorization_header : Hash(String, String) do - _user, header = authentication - header - end -end diff --git a/spec/spec_helpers/client.cr b/spec/spec_helpers/client.cr new file mode 100644 index 00000000..4d1fd44e --- /dev/null +++ b/spec/spec_helpers/client.cr @@ -0,0 +1,8 @@ +module PlaceOS::Api::SpecClient + # Can't use ivars at top level, hence this hack + private CLIENT = ActionController::SpecHelper.client + + def client + CLIENT + end +end diff --git a/spec/core_helper.cr b/spec/spec_helpers/core_helper.cr similarity index 100% rename from spec/core_helper.cr rename to spec/spec_helpers/core_helper.cr diff --git a/spec/http_mocks.cr b/spec/spec_helpers/http_mocks.cr similarity index 100% rename from spec/http_mocks.cr rename to spec/spec_helpers/http_mocks.cr diff --git a/spec/spec_helpers/scopes.cr b/spec/spec_helpers/scopes.cr new file mode 100644 index 00000000..c2ec13df --- /dev/null +++ b/spec/spec_helpers/scopes.cr @@ -0,0 +1,42 @@ +require "../helper" + +module PlaceOS::Api::Scopes + extend self + + def show(base, id, scoped_authorization_header) + client.get( + path: File.join(base, id), + headers: scoped_authorization_header, + ) + end + + def index(path, scoped_authorization_header) + client.get( + path: path, + headers: scoped_authorization_header, + ) + end + + def create(path, body, scoped_authorization_header) + client.post( + path: path, + body: body, + headers: scoped_authorization_header, + ) + end + + def delete(base, id, scoped_authorization_header) + client.delete( + path: File.join(base, id), + headers: scoped_authorization_header, + ) + end + + def update(path, body, scoped_authorization_header) + client.patch( + path: path, + body: body.to_json, + headers: scoped_authorization_header, + ) + end +end diff --git a/spec/spec_helpers/specs.cr b/spec/spec_helpers/specs.cr new file mode 100644 index 00000000..31d4d7aa --- /dev/null +++ b/spec/spec_helpers/specs.cr @@ -0,0 +1,242 @@ +module PlaceOS::Api::Specs + # Check application responds with 404 when model not present + def self.test_404(base, model_name, headers : HTTP::Headers) + it "404s if #{model_name} isn't present in database", tags: "search" do + id = "#{model_name}-#{Random.rand(9999).to_s.ljust(4, '0')}" + path = File.join(base, id) + result = client.get(path, headers: headers) + result.status_code.should eq 404 + end + end + + # Test search on name field + macro test_base_index(klass, controller_klass) + {% klass_name = klass.stringify.split("::").last.underscore %} + + it "queries #{ {{ klass_name }} }", tags: "search" do + _, authorization_header = authentication + doc = PlaceOS::Model::Generator.{{ klass_name.id }} + name = random_name + doc.name = name + doc.save! + + refresh_elastic({{ klass }}.table_name) + + doc.persisted?.should be_true + params = HTTP::Params.encode({"q" => name}) + path = "#{{{controller_klass}}::NAMESPACE[0].rstrip('/')}?#{params}" + header = authorization_header + + found = until_expected("GET", path, header) do |response| + Array(Hash(String, JSON::Any)) + .from_json(response.body) + .map(&.["id"].as_s) + .any?(doc.id) + end + found.should be_true + end + end + + macro test_create(klass, controller_klass) + {% klass_name = klass.stringify.split("::").last.underscore %} + + it "create" do + _, authorization_header = authentication + body = PlaceOS::Model::Generator.{{ klass_name.id }}.to_json + result = client.post( + {{ controller_klass }}.base_route, + body: body, + headers: authorization_header + ) + + result.status_code.should eq 201 + response_model = {{ klass.id }}.from_trusted_json(result.body) + response_model.destroy + end + end + + macro test_show(klass, controller_klass) + {% klass_name = klass.stringify.split("::").last.underscore %} + + it "show" do + _, authorization_header = authentication + model = PlaceOS::Model::Generator.{{ klass_name.id }}.save! + model.persisted?.should be_true + id = model.id.as(String) + result = client.get( + path: File.join({{ controller_klass }}.base_route, id), + headers: authorization_header, + ) + + result.status_code.should eq 200 + response_model = {{ klass.id }}.from_trusted_json(result.body) + response_model.id.should eq id + + model.destroy + end + end + + macro test_destroy(klass, controller_klass) + {% klass_name = klass.stringify.split("::").last.underscore %} + + it "destroy" do + _, authorization_header = authentication + model = PlaceOS::Model::Generator.{{ klass_name.id }}.save! + model.persisted?.should be_true + id = model.id.as(String) + result = client.delete( + path: File.join({{ controller_klass }}.base_route, id), + headers: authorization_header, + ) + + result.status_code.should eq 200 + {{ klass.id }}.find(id).should be_nil + end + end + + macro test_crd(klass, controller_klass) + Specs.test_create({{ klass }}, {{ controller_klass }}) + Specs.test_show({{ klass }}, {{ controller_klass }}) + Specs.test_destroy({{ klass }}, {{ controller_klass }}) + end + + macro test_controller_scope(klass) + {% base = klass.resolve.constant(:NAMESPACE).first %} + + {% if klass.stringify == "Repositories" %} + {% model_name = "Repository" %} + {% model_gen = "repository" %} + {% elsif klass.stringify == "Systems" %} + {% model_name = "ControlSystem" %} + {% model_gen = "control_system" %} + {% elsif klass.stringify == "Settings" %} + {% model_name = "Settings" %} + {% model_gen = "settings" %} + {% else %} + {% model_name = klass.stringify.gsub(/[s]$/, " ").strip %} + {% model_gen = model_name.underscore %} + {% end %} + + {% scope_name = klass.stringify.underscore %} + + context "read" do + it "allows access to show" do + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :read)]) + + model = PlaceOS::Model::Generator.{{ model_gen.id }}.save! + model.persisted?.should be_true + id = model.id.as(String) + result = Scopes.show({{ base }}, id, scoped_authorization_header) + result.status_code.should eq 200 + response_model = Model::{{ model_name.id }}.from_trusted_json(result.body) + response_model.id.should eq id + model.destroy + end + + it "allows access to index" do + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :read)]) + + result = Scopes.index({{ base }}, scoped_authorization_header) + result.success?.should be_true + end + + it "should not allow access to create" do + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :read)]) + + body = PlaceOS::Model::Generator.{{ model_gen.id }}.to_json + result = Scopes.create({{ base }}, body, scoped_authorization_header) + result.status_code.should eq 403 + end + + it "should not allow access to delete" do + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :read)]) + + model = PlaceOS::Model::Generator.{{ model_gen.id }}.save! + model.persisted?.should be_true + id = model.id.as(String) + result = Scopes.delete({{ base }}, id, scoped_authorization_header) + result.status_code.should eq 403 + Model::{{ model_name.id }}.find(id).should_not be_nil + end + end + + context "write" do + it "should not allow access to show" do + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :write)]) + + model = PlaceOS::Model::Generator.{{ model_gen.id }}.save! + model.persisted?.should be_true + id = model.id.as(String) + result = Scopes.show({{ base }}, id, scoped_authorization_header) + result.status_code.should eq 403 + model.destroy + end + + it "should not allow access to index" do + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :write)]) + + result = Scopes.index({{ base }}, scoped_authorization_header) + result.status_code.should eq 403 + end + + it "should allow access to create" do + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :write)]) + + body = PlaceOS::Model::Generator.{{ model_gen.id }}.to_json + result = Scopes.create({{ base }}, body, scoped_authorization_header) + result.success?.should be_true + + response_model = Model::{{ model_name.id }}.from_trusted_json(result.body) + response_model.destroy + end + + it "should allow access to delete" do + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :write)]) + model = PlaceOS::Model::Generator.{{ model_gen.id }}.save! + model.persisted?.should be_true + id = model.id.as(String) + result = Scopes.delete({{ base }}, id, scoped_authorization_header) + result.success?.should be_true + Model::{{ model_name.id }}.find(id).should be_nil + end + end + end + + macro test_update_write_scope(klass) + {% base = klass.resolve.constant(:NAMESPACE).first %} + + {% if klass.stringify == "Repositories" %} + {% model_name = "Repository" %} + {% model_gen = "repository" %} + {% else %} + {% model_name = klass.stringify.gsub(/[s]$/, " ").strip %} + {% model_gen = model_name.underscore %} + {% end %} + + {% scope_name = klass.stringify.underscore %} + + it "checks scope on update" do + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, PlaceOS::Model::UserJWT::Scope::Access::Write)]) + model = Model::Generator.{{ model_gen.id }}.save! + original_name = model.name + model.name = random_name + + id = model.id.as(String) + path = File.join({{ base }}, id) + result = Scopes.update(path, model, scoped_authorization_header) + + result.success?.should be_true + updated = Model::{{ model_name.id }}.from_trusted_json(result.body) + + updated.id.should eq model.id + updated.name.should_not eq original_name + updated.destroy + + _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, PlaceOS::Model::UserJWT::Scope::Access::Read)]) + result = Scopes.update(path, model, scoped_authorization_header) + + result.success?.should be_false + result.status_code.should eq 403 + end + end +end diff --git a/spec/websocket/session_spec.cr b/spec/websocket/session_spec.cr index 43b4ee82..070c33c0 100644 --- a/spec/websocket/session_spec.cr +++ b/spec/websocket/session_spec.cr @@ -1,184 +1,180 @@ -require "../helper" -require "../core_helper" +require "placeos-driver/storage" require "webmock" -require "placeos-driver/storage" +require "../helper" module PlaceOS::Api::WebSocket authenticated_user, authorization_header = authentication - base = Systems::NAMESPACE[0] - - describe Session do - with_server do - describe "systems/control" do - it "opens a websocket session" do - bind(base, authorization_header) do |ws| - ws.closed?.should be_false - end - end - end - describe "websocket API" do - describe "bind" do - it "receives updates" do - # Status to bind - status_name = "nugget" - results = test_websocket_api(base, authorization_header) do |ws, control_system, mod| - # Create a storage proxy - driver_proxy = PlaceOS::Driver::RedisStorage.new mod.id.as(String) - - ws.send Session::Request.new( - id: rand(10).to_i64, - system_id: control_system.id.as(String), - module_name: mod.resolved_name, - name: status_name, - command: Session::Request::Command::Bind, - ).to_json - sleep 100.milliseconds - driver_proxy[status_name] = 1 - sleep 100.milliseconds - driver_proxy[status_name] = 2 - sleep 100.milliseconds - end - - updates, control_system, mod = results - updates.should_not be_empty - - expected_meta = { - sys: control_system.id, - mod: mod.custom_name, - index: 1, - name: status_name, - } - - # Check for successful bind response - updates.first.type.should eq Session::Response::Type::Success - # Check all responses correct metadata - updates.all? { |v| v.metadata == expected_meta }.should be_true - # Check all messages received - updates.size.should eq 3 # Check for status variable updates - updates[1..2].compact_map(&.value.try &.to_i).should eq [1, 2] - end + pending Session do + describe "systems/control" do + it "opens a websocket session" do + bind(Systems.base_route, authorization_header) do |ws| + ws.closed?.should be_false end + end + end - it "unbind" do + describe "websocket API" do + describe "bind" do + it "receives updates" do # Status to bind status_name = "nugget" + results = test_websocket_api(Systems.base_route, authorization_header) do |ws, control_system, mod| + # Create a storage proxy + driver_proxy = PlaceOS::Driver::RedisStorage.new mod.id.as(String) - id = rand(10).to_i64 - results = test_websocket_api(base, authorization_header) do |ws, control_system, mod| - request = { - id: id, - system_id: control_system.id.as(String), + ws.send Session::Request.new( + id: rand(10).to_i64, + system_id: control_system.id.as(String), module_name: mod.resolved_name, - name: status_name, - command: Session::Request::Command::Bind, - } - ws.send Session::Request.new(**request).to_json + name: status_name, + command: Session::Request::Command::Bind, + ).to_json + sleep 100.milliseconds + driver_proxy[status_name] = 1 sleep 100.milliseconds - ws.send Session::Request.new(**request.merge({command: Session::Request::Command::Bind})).to_json + driver_proxy[status_name] = 2 sleep 100.milliseconds end updates, control_system, mod = results - expected_meta = {sys: control_system.id, mod: mod.custom_name, index: 1, name: status_name} - # Check all messages received - updates.size.should eq 2 + updates.should_not be_empty + + expected_meta = { + sys: control_system.id, + mod: mod.custom_name, + index: 1, + name: status_name, + } + + # Check for successful bind response + updates.first.type.should eq Session::Response::Type::Success # Check all responses correct metadata updates.all? { |v| v.metadata == expected_meta }.should be_true - # Check for successful bind response - updates.shift.type.should eq Session::Response::Type::Success - # Check for successful unbind response - updates.shift.type.should eq Session::Response::Type::Success + # Check all messages received + updates.size.should eq 3 # Check for status variable updates + updates[1..2].compact_map(&.value.try &.to_i).should eq [1, 2] end + end - it "exec" do - WebMock.stub(:any, /^http:\/\/core:3000\/api\/core\/v1\/command\//).to_return(body: %({"__exec__":"function2"})) + it "unbind" do + # Status to bind + status_name = "nugget" + + id = rand(10).to_i64 + results = test_websocket_api(Systems.base_route, authorization_header) do |ws, control_system, mod| + request = { + id: id, + system_id: control_system.id.as(String), + module_name: mod.resolved_name, + name: status_name, + command: Session::Request::Command::Bind, + } + ws.send Session::Request.new(**request).to_json + sleep 100.milliseconds + ws.send Session::Request.new(**request.merge({command: Session::Request::Command::Bind})).to_json + sleep 100.milliseconds + end - id = rand(10).to_i64 + updates, control_system, mod = results + expected_meta = {sys: control_system.id, mod: mod.custom_name, index: 1, name: status_name} + # Check all messages received + updates.size.should eq 2 + # Check all responses correct metadata + updates.all? { |v| v.metadata == expected_meta }.should be_true + # Check for successful bind response + updates.shift.type.should eq Session::Response::Type::Success + # Check for successful unbind response + updates.shift.type.should eq Session::Response::Type::Success + end - status_name = "function2" + it "exec" do + WebMock.stub(:any, /^http:\/\/core:3000\/api\/core\/v1\/command\//).to_return(body: %({"__exec__":"function2"})) - id = rand(10).to_i64 - updates, _, _ = test_websocket_exec(base, authorization_header) do |ws, control_system, mod| - request = { - id: id, - system_id: control_system.id.as(String), - module_name: mod.resolved_name, - name: status_name, - command: Session::Request::Command::Exec, - } - ws.send Session::Request.new(**request).to_json - sleep 100.milliseconds - end + id = rand(10).to_i64 - # Check for successful exec response + status_name = "function2" - updates.first.type.should eq Session::Response::Type::Success - updates.first.value.should eq(%({"__exec__":"function2"})) + id = rand(10).to_i64 + updates, _, _ = test_websocket_exec(Systems.base_route, authorization_header) do |ws, control_system, mod| + request = { + id: id, + system_id: control_system.id.as(String), + module_name: mod.resolved_name, + name: status_name, + command: Session::Request::Command::Exec, + } + ws.send Session::Request.new(**request).to_json + sleep 100.milliseconds end - it "debug" do - status_name = "nugget" + # Check for successful exec response - id = rand(10).to_i64 - updates, _, _ = test_websocket_api(base, authorization_header) do |ws, control_system, mod| - request = { - id: id, - system_id: control_system.id.as(String), - module_name: mod.resolved_name, - name: status_name, - command: Session::Request::Command::Debug, - } - ws.send Session::Request.new(**request).to_json - sleep 100.milliseconds - end + updates.first.type.should eq Session::Response::Type::Success + updates.first.value.should eq(%({"__exec__":"function2"})) + end - # Check all messages received - updates.size.should eq 1 - # Check for successful debug response - updates.shift.type.should eq Session::Response::Type::Success + it "debug" do + status_name = "nugget" + + id = rand(10).to_i64 + updates, _, _ = test_websocket_api(Systems.base_route, authorization_header) do |ws, control_system, mod| + request = { + id: id, + system_id: control_system.id.as(String), + module_name: mod.resolved_name, + name: status_name, + command: Session::Request::Command::Debug, + } + ws.send Session::Request.new(**request).to_json + sleep 100.milliseconds end - it "ignore" do - status_name = "nugget" - - id = rand(10).to_i64 - updates, _, _ = test_websocket_api(base, authorization_header) do |ws, control_system, mod| - request = { - id: id, - system_id: control_system.id.as(String), - module_name: mod.resolved_name, - name: status_name, - command: Session::Request::Command::Ignore, - } - ws.send Session::Request.new(**request).to_json - sleep 100.milliseconds - end + # Check all messages received + updates.size.should eq 1 + # Check for successful debug response + updates.shift.type.should eq Session::Response::Type::Success + end - # Check all messages received - updates.size.should eq 1 - # Check for successful ignore response - updates.shift.type.should eq Session::Response::Type::Success + it "ignore" do + status_name = "nugget" + + id = rand(10).to_i64 + updates, _, _ = test_websocket_api(Systems.base_route, authorization_header) do |ws, control_system, mod| + request = { + id: id, + system_id: control_system.id.as(String), + module_name: mod.resolved_name, + name: status_name, + command: Session::Request::Command::Ignore, + } + ws.send Session::Request.new(**request).to_json + sleep 100.milliseconds end + + # Check all messages received + updates.size.should eq 1 + # Check for successful ignore response + updates.shift.type.should eq Session::Response::Type::Success end + end - describe Session::Response do - it "scrubs invalid UTF-8 chars from the error message" do - Session::Response.new( - type: Session::Response::Type::Error, - id: 1234_i64, - message: String.new(Bytes[0xc3, 0x28]), - ).to_json.should contain(Char::REPLACEMENT) - end + describe Session::Response do + it "scrubs invalid UTF-8 chars from the error message" do + Session::Response.new( + type: Session::Response::Type::Error, + id: 1234_i64, + message: String.new(Bytes[0xc3, 0x28]), + ).to_json.should contain(Char::REPLACEMENT) + end - it "scrubs invalid UTF-8 chars from the payload" do - Session::Response.new( - type: Session::Response::Type::Success, - id: 1234_i64, - value: %({"invalid":"#{String.new(Bytes[0xc3, 0x28])}"}) - ).to_json.should contain(Char::REPLACEMENT) - end + it "scrubs invalid UTF-8 chars from the payload" do + Session::Response.new( + type: Session::Response::Type::Success, + id: 1234_i64, + value: %({"invalid":"#{String.new(Bytes[0xc3, 0x28])}"}) + ).to_json.should contain(Char::REPLACEMENT) end end end diff --git a/test b/test index 0ce6c536..338e043d 100755 --- a/test +++ b/test @@ -15,8 +15,6 @@ trap "trap_ctrlc" 2 docker-compose pull -q -docker-compose build -q - exit_code="0" docker-compose run \ From 86171fd130d595afd8bc409477672debe36d44e9 Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Thu, 9 Jun 2022 17:18:49 +1000 Subject: [PATCH 19/29] fix: add `Host` header in specs --- spec/controllers/drivers_spec.cr | 2 +- spec/helper.cr | 21 ++++++--------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/spec/controllers/drivers_spec.cr b/spec/controllers/drivers_spec.cr index 4c15b7a5..b7086fa6 100644 --- a/spec/controllers/drivers_spec.cr +++ b/spec/controllers/drivers_spec.cr @@ -89,7 +89,7 @@ module PlaceOS::Api response.success?.should be_true end - it "POST /:id/recompile" do + pending "POST /:id/recompile" do driver = get_driver response = client.post( diff --git a/spec/helper.cr b/spec/helper.cr index d4b7c74b..6a80fc18 100644 --- a/spec/helper.cr +++ b/spec/helper.cr @@ -80,33 +80,24 @@ end # This method is synchronised due to the redundant top-level calls. def authentication(sys_admin : Bool = true, support : Bool = true, scope = [PlaceOS::Model::UserJWT::Scope::PUBLIC]) CREATION_LOCK.synchronize do - test_user_email = PlaceOS::Model::Email.new("test-admin-#{sys_admin ? "1" : "0"}-supp-#{support ? "1" : "0"}-rest-api@place.tech") - existing = PlaceOS::Model::User.where(email: test_user_email).first? - - authenticated_user = if existing - existing - else - user = PlaceOS::Model::Generator.user - user.sys_admin = sys_admin - user.support = support - user.save! - end + authenticated_user = generate_auth_user(sys_admin, support, scope) - authorization_header = HTTP::Headers{ + headers = HTTP::Headers{ "Authorization" => "Bearer #{PlaceOS::Model::Generator.jwt(authenticated_user, scope).encode}", "Content-Type" => "application/json", + "Host" => "localhost", } - {authenticated_user, authorization_header} + {authenticated_user, headers} end end def generate_auth_user(sys_admin, support, scopes) scope_list = scopes.try &.join('-', &.to_s) + test_user_email = PlaceOS::Model::Email.new("test-#{"admin-" if sys_admin}#{"supp" if support}-scope-#{scope_list}-rest-api@place.tech") - existing = PlaceOS::Model::User.where(email: test_user_email).first? - existing || PlaceOS::Model::Generator.user.tap do |user| + PlaceOS::Model::User.where(email: test_user_email).first? || PlaceOS::Model::Generator.user.tap do |user| user.email = test_user_email user.sys_admin = sys_admin user.support = support From 8b59b14b370c479a90d6022bd2c054502acb6871 Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Thu, 9 Jun 2022 17:39:54 +1000 Subject: [PATCH 20/29] ci: try skip --- .github/workflows/ci.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f20da15..18f1089d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: jobs: test: - uses: PlaceOS/.github/.github/workflows/containerised-test.yml@main + uses: PlaceOS/.github/.github/workflows/containerised-test.yml@fix/skip-non-run-jobs with: todo_issues: true first_commit: 0a1e7680dc203f278f18fbe1f81bfd2713b83d1c diff --git a/README.md b/README.md index 8952d1f1..470a41f1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build](https://github.com/PlaceOS/rest-api/actions/workflows/build.yml/badge.svg)](https://github.com/PlaceOS/rest-api/actions/workflows/build.yml) [![CI](https://github.com/PlaceOS/rest-api/actions/workflows/ci.yml/badge.svg)](https://github.com/PlaceOS/rest-api/actions/workflows/ci.yml) -[![Changelog](https://img.shields.io/badge/Changelog-available-github.svg)](/CHANGELOG.md) +[![Changelog](https://img.shields.io/badge/Changelog-available-github.svg)](./CHANGELOG.md) [PlaceOS](https://place.technology/) service that provides a real-time control API. From d5d322e5f1041ad06f7527ec16421177c208780d Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Thu, 9 Jun 2022 18:02:52 +1000 Subject: [PATCH 21/29] Revert "ci: try skip" This reverts commit 8b59b14b370c479a90d6022bd2c054502acb6871. --- .github/workflows/ci.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18f1089d..1f20da15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: jobs: test: - uses: PlaceOS/.github/.github/workflows/containerised-test.yml@fix/skip-non-run-jobs + uses: PlaceOS/.github/.github/workflows/containerised-test.yml@main with: todo_issues: true first_commit: 0a1e7680dc203f278f18fbe1f81bfd2713b83d1c diff --git a/README.md b/README.md index 470a41f1..8952d1f1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build](https://github.com/PlaceOS/rest-api/actions/workflows/build.yml/badge.svg)](https://github.com/PlaceOS/rest-api/actions/workflows/build.yml) [![CI](https://github.com/PlaceOS/rest-api/actions/workflows/ci.yml/badge.svg)](https://github.com/PlaceOS/rest-api/actions/workflows/ci.yml) -[![Changelog](https://img.shields.io/badge/Changelog-available-github.svg)](./CHANGELOG.md) +[![Changelog](https://img.shields.io/badge/Changelog-available-github.svg)](/CHANGELOG.md) [PlaceOS](https://place.technology/) service that provides a real-time control API. From 0af3b27bd4d48759c18a0489181817921330f3e6 Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Mon, 13 Jun 2022 17:55:08 +1000 Subject: [PATCH 22/29] refactor(spec): remove top-level assignments --- docker-compose.yml | 32 +++++----- shard.lock | 14 ++--- spec/controllers/api_key_spec.cr | 12 ++-- spec/controllers/asset_instances_spec.cr | 16 +++-- spec/controllers/asset_spec.cr | 20 +++--- spec/controllers/brokers_spec.cr | 14 ++--- spec/controllers/drivers_spec.cr | 20 +++--- spec/controllers/edges_spec.cr | 33 +++++----- spec/controllers/metadata_spec.cr | 51 ++++++++-------- spec/controllers/modules_spec.cr | 38 ++++++------ spec/controllers/mqtt_spec.cr | 14 ++--- spec/controllers/repositories_spec.cr | 22 +++---- spec/controllers/root_spec.cr | 16 +++-- spec/controllers/settings_spec.cr | 31 +++++----- spec/controllers/system-triggers_spec.cr | 16 +++-- spec/controllers/systems_spec.cr | 66 ++++++++++---------- spec/controllers/triggers_spec.cr | 19 +++--- spec/controllers/users_spec.cr | 24 ++++---- spec/controllers/zones_spec.cr | 16 +++-- spec/helper.cr | 78 +++++++----------------- spec/spec_helpers/authentication.cr | 75 +++++++++++++++++++++++ spec/spec_helpers/scopes.cr | 20 +++--- spec/spec_helpers/{specs.cr => spec.cr} | 66 ++++++++++---------- spec/websocket/session_spec.cr | 22 +++---- src/constants.cr | 2 +- test | 2 +- 26 files changed, 378 insertions(+), 361 deletions(-) create mode 100644 spec/spec_helpers/authentication.cr rename spec/spec_helpers/{specs.cr => spec.cr} (69%) diff --git a/docker-compose.yml b/docker-compose.yml index 3199acdb..7b667fe4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,7 +42,7 @@ services: - ${PWD}/shard.yml:/app/shard.yml - ${PWD}/coverage:/app/coverage depends_on: - - auth + # - auth - build - core - elastic @@ -50,6 +50,8 @@ services: - redis - rethink - search-ingest + security_opt: + - seccomp:unconfined environment: # Environment GITHUB_ACTION: ${GITHUB_ACTION:-} @@ -115,22 +117,22 @@ services: # Environment <<: *deployment-env - auth: # Authentication Service - image: placeos/auth:nightly - restart: always - hostname: auth - depends_on: - - redis - - rethink - environment: - <<: *rethinkdb-client-env - <<: *redis-client-env - COAUTH_NO_SSL: "true" - TZ: $TZ - PLACE_URI: https://${PLACE_DOMAIN:-localhost:8443} + # auth: # Authentication Service + # image: placeos/auth:nightly + # restart: always + # hostname: auth + # depends_on: + # - redis + # - rethink + # environment: + # <<: *rethinkdb-client-env + # <<: *redis-client-env + # COAUTH_NO_SSL: "true" + # TZ: $TZ + # PLACE_URI: https://${PLACE_DOMAIN:-localhost:8443} core: # Module coordinator - image: ghcr.io/placeos/core:feat-build + image: placeos/core:feat-build restart: always hostname: core depends_on: diff --git a/shard.lock b/shard.lock index f0aaddc8..9b5ae971 100644 --- a/shard.lock +++ b/shard.lock @@ -79,11 +79,11 @@ shards: debug: git: https://github.com/sija/debug.cr.git - version: 2.0.1 + version: 2.0.2 defined: git: https://github.com/wyhaines/defined.cr.git - version: 0.3.4 + version: 0.3.6 dtls: git: https://github.com/spider-gazelle/crystal-dtls.git @@ -187,7 +187,7 @@ shards: opentelemetry-instrumentation: git: https://github.com/wyhaines/opentelemetry-instrumentation.cr.git - version: 0.3.6+git.commit.64fa1db57f535c511e6515620e7f8e4ce8a72780 + version: 0.3.5 pars: # Overridden git: https://github.com/spider-gazelle/pars.git @@ -203,7 +203,7 @@ shards: placeos-build: git: https://github.com/placeos/build.git - version: 1.0.3+git.commit.67e7e5aebe872b8d7369fa572e55e2be26d94503 + version: 1.0.3+git.commit.9cebd6e63b0466d5c2e580dbf67854694a54f799 placeos-compiler: git: https://github.com/placeos/compiler.git @@ -211,7 +211,7 @@ shards: placeos-core: git: https://github.com/placeos/core.git - version: 4.3.1+git.commit.6995c2ae32c74b9a44d7875ff2f55abd5603bbad + version: 4.3.1+git.commit.516813c454424c19fe68372dc1d685180a745064 placeos-core-client: # Overridden git: https://github.com/placeos/core-client.git @@ -227,7 +227,7 @@ shards: placeos-log-backend: git: https://github.com/place-labs/log-backend.git - version: 0.10.3 + version: 0.10.5 placeos-models: git: https://github.com/placeos/models.git @@ -251,7 +251,7 @@ shards: raven: git: https://github.com/sija/raven.cr.git - version: 1.9.1+git.commit.d53319dc4ce26fc44f13b9c1c6ffb8699e22899b + version: 1.9.2+git.commit.de91bb38858124270ea06be61641153b8947e18f redis: git: https://github.com/stefanwille/crystal-redis.git diff --git a/spec/controllers/api_key_spec.cr b/spec/controllers/api_key_spec.cr index 3e644f3a..c5fb0f15 100644 --- a/spec/controllers/api_key_spec.cr +++ b/spec/controllers/api_key_spec.cr @@ -1,21 +1,21 @@ require "../helper" module PlaceOS::Api - describe ApiKeys do - _, scoped_authorization_header = x_api_authentication + _, scoped_headers = Spec::Authentication.x_api_authentication - Specs.test_404(ApiKeys.base_route, model_name: Model::ApiKey.table_name, headers: scoped_authorization_header) + describe ApiKeys do + Spec.test_404(ApiKeys.base_route, model_name: Model::ApiKey.table_name, headers: scoped_headers) describe "index", tags: "search" do - Specs.test_base_index(Model::ApiKey, ApiKeys) + Spec.test_base_index(Model::ApiKey, ApiKeys) end describe "CRUD operations", tags: "crud" do - Specs.test_crd(Model::ApiKey, ApiKeys) + Spec.test_crd(Model::ApiKey, ApiKeys) end describe "scopes" do - Specs.test_controller_scope(ApiKeys) + Spec.test_controller_scope(ApiKeys) end end end diff --git a/spec/controllers/asset_instances_spec.cr b/spec/controllers/asset_instances_spec.cr index 0d32a906..01c58540 100644 --- a/spec/controllers/asset_instances_spec.cr +++ b/spec/controllers/asset_instances_spec.cr @@ -3,16 +3,14 @@ require "timecop" module PlaceOS::Api describe AssetInstances do - _, authorization_header = authentication - - Specs.test_404( + Spec.test_404( AssetInstances.base_route, model_name: Model::AssetInstance.table_name, - headers: authorization_header, + headers: Spec::Authentication.headers, ) describe "index", tags: "search" do - Specs.test_base_index(klass: Model::AssetInstance, controller_klass: AssetInstances) + Spec.test_base_index(klass: Model::AssetInstance, controller_klass: PlaceOS::Api::AssetInstances) end describe "CRUD operations", tags: "crud" do @@ -23,7 +21,7 @@ module PlaceOS::Api result = client.post( path: AssetInstances.base_route, body: body, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 201 @@ -37,7 +35,7 @@ module PlaceOS::Api result = client.get( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) fetched = Model::AssetInstance.from_trusted_json(result.body) @@ -53,7 +51,7 @@ module PlaceOS::Api result = client.patch( path: path, body: {approval: true}.to_json, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -71,7 +69,7 @@ module PlaceOS::Api id = model.id.not_nil! path = File.join(AssetInstances.base_route, id) - result = client.delete(path: path, headers: authorization_header) + result = client.delete(path: path, headers: Spec::Authentication.headers) result.status_code.should eq 200 Model::AssetInstance.find(id.as(String)).should be_nil diff --git a/spec/controllers/asset_spec.cr b/spec/controllers/asset_spec.cr index 7b398a6a..5487eca3 100644 --- a/spec/controllers/asset_spec.cr +++ b/spec/controllers/asset_spec.cr @@ -2,12 +2,10 @@ require "../helper" module PlaceOS::Api describe Assets do - _, authorization_header = authentication - - Specs.test_404(Assets.base_route, model_name: Model::Asset.table_name, headers: authorization_header) + Spec.test_404(Assets.base_route, model_name: Model::Asset.table_name, headers: Spec::Authentication.headers) describe "index", tags: "search" do - Specs.test_base_index(klass: Model::Asset, controller_klass: Assets) + Spec.test_base_index(klass: Model::Asset, controller_klass: Assets) end describe "GET /asset-instances/:id/instances" do @@ -17,7 +15,7 @@ module PlaceOS::Api response = client.get( path: File.join(Assets.base_route, asset.id.not_nil!, "asset_instances"), - headers: authorization_header, + headers: Spec::Authentication.headers, ) # Can't use from_json directly on the model as `id` will not be parsed @@ -28,7 +26,7 @@ module PlaceOS::Api end describe "CRUD operations", tags: "crud" do - Specs.test_crd(klass: Model::Asset, controller_klass: Assets) + Spec.test_crd(klass: Model::Asset, controller_klass: Assets) end it "update" do @@ -42,7 +40,7 @@ module PlaceOS::Api result = client.patch( path: path, body: asset.to_json, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -60,11 +58,11 @@ module PlaceOS::Api asset_instance_id = asset_instance.id.as(String) params = HTTP::Params{"instances" => "true"} - path = "#{Assets.base}#{asset.id}?#{params}" + path = "#{Assets.base_route}#{asset.id}?#{params}" result = client.get( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) response = JSON.parse(result.body) @@ -74,7 +72,7 @@ module PlaceOS::Api end describe "scopes" do - Specs.test_controller_scope(Assets) - Specs.test_update_write_scope(Assets) + Spec.test_controller_scope(Assets) + Spec.test_update_write_scope(Assets) end end diff --git a/spec/controllers/brokers_spec.cr b/spec/controllers/brokers_spec.cr index 934cade3..515d5fae 100644 --- a/spec/controllers/brokers_spec.cr +++ b/spec/controllers/brokers_spec.cr @@ -2,16 +2,14 @@ require "../helper" module PlaceOS::Api describe Brokers do - _, authorization_header = authentication - - Specs.test_404(Brokers.base_route, model_name: Model::Broker.table_name, headers: authorization_header) + Spec.test_404(Brokers.base_route, model_name: Model::Broker.table_name, headers: Spec::Authentication.headers) describe "index", tags: "search" do - Specs.test_base_index(klass: Model::Broker, controller_klass: Brokers) + Spec.test_base_index(klass: Model::Broker, controller_klass: Brokers) end describe "CRUD operations", tags: "crud" do - Specs.test_crd(Model::Broker, Brokers) + Spec.test_crd(Model::Broker, Brokers) it "update" do broker = Model::Generator.broker.save! @@ -23,7 +21,7 @@ module PlaceOS::Api result = client.patch( path: path, body: broker.changed_attributes.to_json, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -35,8 +33,8 @@ module PlaceOS::Api end describe "scopes" do - Specs.test_update_write_scope(Brokers) - Specs.test_controller_scope(Brokers) + Spec.test_update_write_scope(Brokers) + Spec.test_controller_scope(Brokers) end end end diff --git a/spec/controllers/drivers_spec.cr b/spec/controllers/drivers_spec.cr index b7086fa6..24fd1a3b 100644 --- a/spec/controllers/drivers_spec.cr +++ b/spec/controllers/drivers_spec.cr @@ -2,10 +2,8 @@ require "../helper" module PlaceOS::Api describe Drivers do - _, authorization_header = authentication - describe "index", tags: "search" do - Specs.test_base_index(klass: Model::Driver, controller_klass: Drivers) + Spec.test_base_index(klass: Model::Driver, controller_klass: Drivers) it "filters queries by driver role" do service = Model::Generator.driver(role: Model::Driver::Role::Service) @@ -19,7 +17,7 @@ module PlaceOS::Api refresh_elastic(Model::Driver.table_name) path = "#{Drivers.base_route}?#{params}" - found = until_expected("GET", path, authorization_header) do |response| + found = until_expected("GET", path, Spec::Authentication.headers) do |response| results = Array(Hash(String, JSON::Any)).from_json(response.body) all_service_roles = results.all? { |r| r["role"] == Model::Driver::Role::Service.to_i } contains_search_term = results.any? { |r| r["id"] == service.id } @@ -30,14 +28,14 @@ module PlaceOS::Api end end - Specs.test_404(Drivers.base_route, model_name: Model::Driver.table_name, headers: authorization_header) + Spec.test_404(Drivers.base_route, model_name: Model::Driver.table_name, headers: Spec::Authentication.headers) describe "CRUD operations", tags: "crud" do before_each do HttpMocks.reset end - Specs.test_crd(klass: Model::Driver, controller_klass: Drivers) + Spec.test_crd(klass: Model::Driver, controller_klass: Drivers) describe "update" do it "if role is preserved" do @@ -50,7 +48,7 @@ module PlaceOS::Api result = client.patch( path: path, body: driver.changed_attributes.to_json, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.success?.should be_true @@ -67,7 +65,7 @@ module PlaceOS::Api result = client.patch( path: path, body: driver.changed_attributes.to_json, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.success?.should_not be_true @@ -83,7 +81,7 @@ module PlaceOS::Api response = client.get( path: "#{Drivers.base_route}#{driver.id.not_nil!}/compiled", - headers: authorization_header, + headers: Spec::Authentication.headers, ) response.success?.should be_true @@ -94,7 +92,7 @@ module PlaceOS::Api response = client.post( path: "#{Drivers.base_route}#{driver.id.not_nil!}/recompile", - headers: authorization_header, + headers: Spec::Authentication.headers, ) response.success?.should be_true @@ -108,7 +106,7 @@ module PlaceOS::Api HttpMocks.core_compiled end - Specs.test_controller_scope(Drivers) + Spec.test_controller_scope(Drivers) end end end diff --git a/spec/controllers/edges_spec.cr b/spec/controllers/edges_spec.cr index b4fa1cb5..fac6c384 100644 --- a/spec/controllers/edges_spec.cr +++ b/spec/controllers/edges_spec.cr @@ -3,46 +3,45 @@ require "placeos-core/placeos-edge/client" module PlaceOS::Api describe Edges do - authenticated_user, authorization_header = authentication - Specs.test_404(Edges.base_route, model_name: Model::Edge.table_name, headers: authorization_header) + Spec.test_404(Edges.base_route, model_name: Model::Edge.table_name, headers: Spec::Authentication.headers) describe "index", tags: "search" do - Specs.test_base_index(Model::Edge, Edges) + Spec.test_base_index(Model::Edge, Edges) end describe "/control" do it "authenticates with an API key from a new edge" do # Create a new edge to test with as the controller would - edge_name = "Test Edge" edge_host = "localhost" - edge_port = 6000 - create_body = Model::Edge::CreateBody.new(name: edge_name, user_id: authenticated_user.id.as(String)) new_edge = Model::Edge.for_user( - user: authenticated_user, - name: create_body.name, - description: create_body.description + user: Spec::Authentication.user, + name: random_name, ) # Ensure instance variable initialised and edge saved new_edge.x_api_key new_edge.save! - uri = URI.new(host: edge_host, port: edge_port, query: "api-key=#{new_edge.x_api_key}") - client = PlaceOS::Edge::Client.new( + path = "#{Edges.base_route}/control" + + uri = URI.new(host: edge_host, path: path, query: URI::Params{"api-key" => new_edge.x_api_key}) + + edge_client = PlaceOS::Edge::Client.new( uri: uri, secret: new_edge.x_api_key ) - client.connect do - client.transport.closed?.should_not be_nil - client.disconnect + websocket = client.establish_ws(path, headers: HTTP::Headers{"Host" => edge_host}) + edge_client.connect(websocket) do + edge_client.transport.closed?.should be_false + edge_client.disconnect end end end describe "CRUD operations", tags: "crud" do - Specs.test_crd(Model::Edge, Edges) + Spec.test_crd(Model::Edge, Edges) describe "create" do it "contains the api token in the response" do @@ -52,7 +51,7 @@ module PlaceOS::Api "description" => "", "name" => "test-edge", }.to_json, - headers: authorization_header, + headers: Spec::Authentication.headers, ) JSON.parse(result.body)["x_api_key"]?.try(&.as_s?).should_not be_nil @@ -61,7 +60,7 @@ module PlaceOS::Api end describe "scopes" do - Specs.test_controller_scope(Edges) + Spec.test_controller_scope(Edges) end end end diff --git a/spec/controllers/metadata_spec.cr b/spec/controllers/metadata_spec.cr index 5ba32d5d..5810af0d 100644 --- a/spec/controllers/metadata_spec.cr +++ b/spec/controllers/metadata_spec.cr @@ -3,8 +3,6 @@ require "timecop" module PlaceOS::Api describe Metadata do - _authenticated_user, authorization_header = authentication - describe "GET /metadata/:id/children/" do it "shows zone children metadata" do parent = Model::Generator.zone.save! @@ -19,7 +17,7 @@ module PlaceOS::Api result = client.get( path: "#{Metadata.base_route}/#{parent_id}/children", - headers: authorization_header, + headers: Spec::Authentication.headers, ) Array(NamedTuple(zone: JSON::Any, metadata: Hash(String, Model::Metadata::Interface))) @@ -48,7 +46,7 @@ module PlaceOS::Api result = client.get( path: "#{Metadata.base_route}/#{parent_id}/children?name=special", - headers: authorization_header, + headers: Spec::Authentication.headers, ) Array(NamedTuple(zone: JSON::Any, metadata: Hash(String, Model::Metadata::Interface))) @@ -61,8 +59,9 @@ module PlaceOS::Api end describe "PUT /metadata" do - it "creates metadata" do + it "creates metadata", focus: true do parent = Model::Generator.zone.save! + meta = Model::Metadata::Interface.new( name: "test", description: "", @@ -72,12 +71,12 @@ module PlaceOS::Api ) parent_id = parent.id.as(String) - path = "#{Metadata.base_route}/#{parent_id}" + path = "#{Api::Metadata.base_route}/#{parent_id}" result = client.put( path: path, body: meta.to_json, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 201 @@ -103,7 +102,7 @@ module PlaceOS::Api result = client.put( path: path, body: meta.to_json, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 201 @@ -123,7 +122,7 @@ module PlaceOS::Api result = client.put( path: path, body: updated_meta.to_json, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -144,7 +143,7 @@ module PlaceOS::Api result = client.get( path: "#{Metadata.base_route}/#{control_system_id}", - headers: authorization_header, + headers: Spec::Authentication.headers, ) metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) @@ -162,7 +161,7 @@ module PlaceOS::Api result = client.get( path: "#{Metadata.base_route}/#{control_system_id}?name=special", - headers: authorization_header, + headers: Spec::Authentication.headers, ) metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) @@ -176,7 +175,7 @@ module PlaceOS::Api result = client.get( path: "#{Metadata.base_route}/#{zone_id}", - headers: authorization_header, + headers: Spec::Authentication.headers, ) metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) @@ -194,7 +193,7 @@ module PlaceOS::Api result = client.get( path: "#{Metadata.base_route}/#{zone_id}?name=special", - headers: authorization_header, + headers: Spec::Authentication.headers, ) metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) @@ -219,7 +218,7 @@ module PlaceOS::Api result = client.get( path: File.join(Metadata.base_route, metadata.parent_id.as(String), "history"), - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -234,7 +233,7 @@ module PlaceOS::Api scope_name = "metadata" it "allows access to show" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new(scope_name, :read)]) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new(scope_name, :read)]) parent = Model::Generator.zone.save! parent_id = parent.id.as(String) @@ -248,7 +247,7 @@ module PlaceOS::Api result = client.get( path: "#{Metadata.base_route}/#{parent_id}/children", - headers: scoped_authorization_header, + headers: scoped_headers, ) result.status_code.should eq 200 Array(NamedTuple(zone: JSON::Any, metadata: Hash(String, Model::Metadata::Interface))) @@ -261,7 +260,7 @@ module PlaceOS::Api end it "should not allow access to delete" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new(scope_name, :read)]) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new(scope_name, :read)]) parent = Model::Generator.zone.save! parent_id = parent.id.as(String) @@ -277,7 +276,7 @@ module PlaceOS::Api result = client.delete( path: "#{Metadata.base_route}/#{id}", - headers: scoped_authorization_header, + headers: scoped_headers, ) result.status_code.should eq 403 end @@ -287,7 +286,7 @@ module PlaceOS::Api scope_name = "metadata" it "should allow access to update" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new(scope_name, :write)]) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new(scope_name, :write)]) parent = Model::Generator.zone.save! meta = Model::Metadata::Interface.new( @@ -304,7 +303,7 @@ module PlaceOS::Api result = client.put( path: path, body: meta.to_json, - headers: scoped_authorization_header, + headers: scoped_headers, ) new_metadata = Model::Metadata::Interface.from_json(result.body) @@ -313,7 +312,7 @@ module PlaceOS::Api end it "should not allow access to show" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new(scope_name, :write)]) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new(scope_name, :write)]) parent = Model::Generator.zone.save! parent_id = parent.id.as(String) @@ -327,14 +326,14 @@ module PlaceOS::Api result = client.get( path: "#{Metadata.base_route}/#{parent_id}/children", - headers: scoped_authorization_header, + headers: scoped_headers, ) result.status_code.should eq 403 end end it "checks that guests can read metadata" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("guest", PlaceOS::Model::UserJWT::Scope::Access::Read)]) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("guest", PlaceOS::Model::UserJWT::Scope::Access::Read)]) zone = Model::Generator.zone.save! zone_id = zone.id.as(String) @@ -342,7 +341,7 @@ module PlaceOS::Api result = client.get( path: "#{Metadata.base_route}/#{zone_id}", - headers: scoped_authorization_header, + headers: scoped_headers, ) result.status_code.should eq 200 @@ -353,14 +352,14 @@ module PlaceOS::Api end it "checks that guests cannot write metadata" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("guest", PlaceOS::Model::UserJWT::Scope::Access::Read)]) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("guest", PlaceOS::Model::UserJWT::Scope::Access::Read)]) zone = Model::Generator.zone.save! zone_id = zone.id.as(String) result = client.post( path: "#{Metadata.base_route}/#{zone_id}", - headers: scoped_authorization_header, + headers: scoped_headers, ) result.success?.should be_false end diff --git a/spec/controllers/modules_spec.cr b/spec/controllers/modules_spec.cr index cddd0cbe..0adb6fc3 100644 --- a/spec/controllers/modules_spec.cr +++ b/spec/controllers/modules_spec.cr @@ -3,11 +3,10 @@ require "timecop" module PlaceOS::Api describe Modules do - _authenticated_user, authorization_header = authentication - Specs.test_404(Modules.base_route, model_name: Model::Module.table_name, headers: authorization_header) + Spec.test_404(Modules.base_route, model_name: Model::Module.table_name, headers: Spec::Authentication.headers) describe "CRUD operations", tags: "crud" do - Specs.test_crd(klass: Model::Module, controller_klass: Modules) + Spec.test_crd(klass: Model::Module, controller_klass: Modules) it "update preserves logic module connection status" do driver = Model::Generator.driver(role: Model::Driver::Role::Logic).save! @@ -21,7 +20,7 @@ module PlaceOS::Api result = client.patch( path: path, body: mod.to_json, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -43,7 +42,7 @@ module PlaceOS::Api result = client.patch( path: path, body: mod.to_json, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -69,7 +68,7 @@ module PlaceOS::Api params = HTTP::Params.encode({"q" => name}) path = "#{Modules.base_route.rstrip('/')}?#{params}" - header = authorization_header + header = Spec::Authentication.headers found = until_expected("GET", path, header) do |response| Array(Hash(String, JSON::Any)).from_json(response.body).any? do |result| result["id"].as_s == doc.id @@ -90,6 +89,7 @@ module PlaceOS::Api "#{Modules.base_route}?#{HTTP::Params{"control_system_id" => sys.id.as(String)}}" ) + response.status.success?.should be_true response.headers["X-Total-Count"].should eq("1") Array(Hash(String, JSON::Any)).from_json(response.body.to_s).map(&.["id"].as_s).first?.should eq(mod.id) end @@ -111,7 +111,7 @@ module PlaceOS::Api params = HTTP::Params.encode({"as_of" => (mod1.updated_at.try &.to_unix).to_s}) path = "#{Modules.base_route}?#{params}" - found = until_expected("GET", path, authorization_header) do |response| + found = until_expected("GET", path, Spec::Authentication.headers) do |response| results = Array(Hash(String, JSON::Any)).from_json(response.body).map(&.["id"].as_s) contains_correct = results.any?(mod1.id) contains_incorrect = results.any?(mod2.id) @@ -131,7 +131,7 @@ module PlaceOS::Api params = HTTP::Params.encode({"connected" => "true"}) path = "#{Modules.base_route}?#{params}" - found = until_expected("GET", path, authorization_header) do |response| + found = until_expected("GET", path, Spec::Authentication.headers) do |response| results = Array(Hash(String, JSON::Any)).from_json(response.body) all_connected = results.all? { |r| r["connected"].as_bool == true } @@ -152,7 +152,7 @@ module PlaceOS::Api params = HTTP::Params.encode({"no_logic" => "true"}) path = "#{Modules.base_route}?#{params}" - found = until_expected("GET", path, authorization_header) do |response| + found = until_expected("GET", path, Spec::Authentication.headers) do |response| results = Array(Hash(String, JSON::Any)).from_json(response.body) no_logic = results.all? { |r| r["role"].as_i != Model::Driver::Role::Logic.to_i } @@ -197,7 +197,7 @@ module PlaceOS::Api path = "#{Modules.base_route}#{mod.id}/settings" result = client.get( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.success?.should be_true @@ -224,7 +224,7 @@ module PlaceOS::Api result = client.get( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) unless result.success? @@ -242,7 +242,7 @@ module PlaceOS::Api result = client.get( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) unless result.success? @@ -260,7 +260,7 @@ module PlaceOS::Api path = "#{Modules.base_route}#{mod.id}/ping" result = client.post( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.success?.should be_false @@ -278,7 +278,7 @@ module PlaceOS::Api path = "#{Modules.base_route}#{mod.id}/ping" result = client.post( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) body = JSON.parse(result.body) @@ -287,10 +287,10 @@ module PlaceOS::Api end describe "scopes" do - Specs.test_controller_scope(Modules) + Spec.test_controller_scope(Modules) it "checks scope on update" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("modules", PlaceOS::Model::UserJWT::Scope::Access::Write)]) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("modules", PlaceOS::Model::UserJWT::Scope::Access::Write)]) driver = Model::Generator.driver(role: Model::Driver::Role::Service).save! mod = Model::Generator.module(driver: driver).save! @@ -300,15 +300,15 @@ module PlaceOS::Api id = mod.id.as(String) path = File.join(Modules.base_route, id) - result = Scopes.update(path, mod, scoped_authorization_header) + result = Scopes.update(path, mod, scoped_headers) result.status_code.should eq 200 updated = Model::Module.from_trusted_json(result.body) updated.id.should eq mod.id updated.connected.should eq !connected - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("modules", PlaceOS::Model::UserJWT::Scope::Access::Read)]) - result = Scopes.update(path, mod, scoped_authorization_header) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("modules", PlaceOS::Model::UserJWT::Scope::Access::Read)]) + result = Scopes.update(path, mod, scoped_headers) result.success?.should be_false result.status_code.should eq 403 diff --git a/spec/controllers/mqtt_spec.cr b/spec/controllers/mqtt_spec.cr index 3630f147..cab2a7d6 100644 --- a/spec/controllers/mqtt_spec.cr +++ b/spec/controllers/mqtt_spec.cr @@ -1,11 +1,11 @@ require "../helper" module PlaceOS::Api - describe MQTT do - scope = [PlaceOS::Model::UserJWT::Scope.new("mqtt", :write)] - authenticated_user, _scoped_authorization_header = authentication(scope: scope) - user_jwt = PlaceOS::Model::Generator.jwt(authenticated_user, scope) + scope = [PlaceOS::Model::UserJWT::Scope.new("mqtt", :write)] + authenticated_user, _scoped_headers = Spec::Authentication.authentication(scope: scope) + user_jwt = PlaceOS::Model::Generator.jwt(authenticated_user, scope) + describe MQTT do describe "MQTT Access" do describe ".mqtt_acl_status" do it "denies access for #{MQTT::MqttAcl::None} access" do @@ -24,20 +24,20 @@ module PlaceOS::Api it "allows #{MQTT::MqttAcl::Read} access" do scope = [PlaceOS::Model::UserJWT::Scope.new("mqtt", :read)] - authenticated_user, _scoped_authorization_header = authentication(scope: scope) + authenticated_user, _scoped_headers = Spec::Authentication.authentication(scope: scope) user_jwt = PlaceOS::Model::Generator.jwt(authenticated_user, scope) MQTT.mqtt_acl_status(MQTT::MqttAcl::Read, user_jwt).should eq HTTP::Status::OK end it "allows #{MQTT::MqttAcl::Write} access for support and above" do scope = [PlaceOS::Model::UserJWT::Scope.new("mqtt", :write)] - authenticated_user, _scoped_authorization_header = authentication(scope: scope) + authenticated_user, _scoped_headers = Spec::Authentication.authentication(scope: scope) user_jwt = PlaceOS::Model::Generator.jwt(authenticated_user, scope) MQTT.mqtt_acl_status(MQTT::MqttAcl::Write, user_jwt).should eq HTTP::Status::OK end it "denies #{MQTT::MqttAcl::Write} access for under support" do - authenticated_user, _ = authentication(sys_admin: false, support: false, scope: scope) + authenticated_user, _ = Spec::Authentication.authentication(sys_admin: false, support: false, scope: scope) user_jwt = PlaceOS::Model::Generator.jwt(authenticated_user, scope) MQTT.mqtt_acl_status(MQTT::MqttAcl::Write, user_jwt).should eq HTTP::Status::FORBIDDEN end diff --git a/spec/controllers/repositories_spec.cr b/spec/controllers/repositories_spec.cr index 2e9a0b1d..89e1ec42 100644 --- a/spec/controllers/repositories_spec.cr +++ b/spec/controllers/repositories_spec.cr @@ -2,16 +2,14 @@ require "../helper" module PlaceOS::Api describe Repositories do - _authenticated_user, authorization_header = authentication - - Specs.test_404(Repositories.base_route, model_name: Model::Repository.table_name, headers: authorization_header) + Spec.test_404(Repositories.base_route, model_name: Model::Repository.table_name, headers: Spec::Authentication.headers) describe "index", tags: "search" do - Specs.test_base_index(Model::Repository, Repositories) + Spec.test_base_index(Model::Repository, Repositories) end describe "CRUD operations", tags: "crud" do - Specs.test_crd(Model::Repository, Repositories) + Spec.test_crd(Model::Repository, Repositories) it "update" do repository = Model::Generator.repository.save! @@ -23,7 +21,7 @@ module PlaceOS::Api result = client.patch( path: path, body: repository.changed_attributes.to_json, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -42,7 +40,7 @@ module PlaceOS::Api result = client.patch( path: path, body: {uri: "https://changed:8080"}.to_json, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 422 @@ -56,7 +54,7 @@ module PlaceOS::Api result = client.patch( path: path, body: {uri: "https://changed:8080"}.to_json, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -74,7 +72,7 @@ module PlaceOS::Api path = "#{Repositories.base_route}#{id}/drivers" result = client.get( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status.should eq HTTP::Status::BAD_REQUEST @@ -85,7 +83,7 @@ module PlaceOS::Api path = "#{Repositories.base_route}#{id}/details" result = client.get( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status.should eq HTTP::Status::BAD_REQUEST @@ -125,8 +123,8 @@ module PlaceOS::Api end describe "scopes" do - Specs.test_controller_scope(Repositories) - Specs.test_update_write_scope(Repositories) + Spec.test_controller_scope(Repositories) + Spec.test_update_write_scope(Repositories) end end end diff --git a/spec/controllers/root_spec.cr b/spec/controllers/root_spec.cr index 5accea4f..64277fc4 100644 --- a/spec/controllers/root_spec.cr +++ b/spec/controllers/root_spec.cr @@ -2,18 +2,16 @@ require "../helper" module PlaceOS::Api describe Root do - _authenticated_user, authorization_header = authentication - describe "GET /" do it "responds to health checks" do - result = client.get(Root.base_route, headers: authorization_header) + result = client.get(Root.base_route, headers: Spec::Authentication.headers) result.status_code.should eq 200 end end describe "GET /scopes" do it "gets scope names" do - result = client.get(File.join(Root.base_route, "scopes"), headers: authorization_header) + result = client.get(File.join(Root.base_route, "scopes"), headers: Spec::Authentication.headers) scopes = Array(String).from_json(result.body) scopes.size.should eq(Root.scopes.size) end @@ -31,7 +29,7 @@ module PlaceOS::Api describe "GET /version" do it "renders version" do - result = client.get(File.join(Root.base_route, "version"), headers: authorization_header) + result = client.get(File.join(Root.base_route, "version"), headers: Spec::Authentication.headers) result.status_code.should eq 200 response = PlaceOS::Model::Version.from_json(result.body) @@ -44,7 +42,7 @@ module PlaceOS::Api describe "GET /platform" do it "renders platform information" do - result = client.get(File.join(Root.base_route, "platform"), headers: authorization_header) + result = client.get(File.join(Root.base_route, "platform"), headers: Spec::Authentication.headers) result.status_code.should eq 200 response = PlaceOS::Api::Root::PlatformInfo.from_json(result.body) @@ -64,7 +62,7 @@ module PlaceOS::Api end params = HTTP::Params{"channel" => subscription_channel} - result = client.post(File.join(Root.base_route, "signal?#{params}"), body: "hello", headers: authorization_header) + result = client.post(File.join(Root.base_route, "signal?#{params}"), body: "hello", headers: Spec::Authentication.headers) result.status_code.should eq 200 begin @@ -80,12 +78,12 @@ module PlaceOS::Api end it "validates presence of `channel` param" do - result = client.post(File.join(Root.base_route, "signal"), body: "hello", headers: authorization_header) + result = client.post(File.join(Root.base_route, "signal"), body: "hello", headers: Spec::Authentication.headers) result.status_code.should eq 400 end context "guest users" do - _, guest_header = authentication(sys_admin: false, support: false, scope: [PlaceOS::Model::UserJWT::Scope::GUEST]) + _, guest_header = Spec::Authentication.authentication(sys_admin: false, support: false, scope: [PlaceOS::Model::UserJWT::Scope::GUEST]) it "prevented access to non-guest channels " do result = client.post(File.join(Root.base_route, "signal?channel=dummy"), body: "hello", headers: guest_header) diff --git a/spec/controllers/settings_spec.cr b/spec/controllers/settings_spec.cr index 320e7317..1d8ae176 100644 --- a/spec/controllers/settings_spec.cr +++ b/spec/controllers/settings_spec.cr @@ -2,13 +2,12 @@ require "../helper" module PlaceOS::Api describe Settings do - _, authorization_header = authentication - Specs.test_404(Settings.base_route, model_name: Model::Settings.table_name, headers: authorization_header) + Spec.test_404(Settings.base_route, model_name: Model::Settings.table_name, headers: Spec::Authentication.headers) describe "support user" do context "access" do it "index" do - _, support_header = authentication(sys_admin: false, support: true) + _, support_header = Spec::Authentication.authentication(sys_admin: false, support: true) sys = Model::Generator.control_system.save! setting = Model::Generator.settings(encryption_level: Encryption::Level::None, control_system: sys) setting.settings_string = "tree: 1" @@ -22,7 +21,7 @@ module PlaceOS::Api end it "show" do - _, support_header = authentication(sys_admin: false, support: true) + _, support_header = Spec::Authentication.authentication(sys_admin: false, support: true) sys = Model::Generator.control_system.save! setting = Model::Generator.settings(encryption_level: Encryption::Level::None, control_system: sys) setting.settings_string = "tree: 1" @@ -50,7 +49,7 @@ module PlaceOS::Api result = client.get( path: path, - headers: authorization_header + headers: Spec::Authentication.headers ) result.status_code.should eq 200 @@ -75,7 +74,7 @@ module PlaceOS::Api result = client.get( path: File.join(Settings.base_route, "?parent_id=#{sys.id},#{sys2.id}"), - headers: authorization_header + headers: Spec::Authentication.headers ) result.status_code.should eq 200 @@ -103,7 +102,7 @@ module PlaceOS::Api result = client.get( path: File.join(Settings.base_route, "?parent_id=#{sys.id}"), - headers: authorization_header + headers: Spec::Authentication.headers ) result.status_code.should eq 200 @@ -140,7 +139,7 @@ module PlaceOS::Api result = client.get( path: File.join(Settings.base_route, "/#{setting.id}/history"), - headers: authorization_header + headers: Spec::Authentication.headers ) result.success?.should be_true @@ -151,7 +150,7 @@ module PlaceOS::Api result = client.get( path: File.join(Settings.base_route, "/#{setting.id}/history?limit=1"), - headers: authorization_header + headers: Spec::Authentication.headers ) link = %(; rel="next") @@ -165,7 +164,7 @@ module PlaceOS::Api end describe "CRUD operations", tags: "crud" do - Specs.test_crd(klass: Model::Settings, controller_klass: Settings) + Spec.test_crd(klass: Model::Settings, controller_klass: Settings) it "update" do settings = Model::Generator.settings(encryption_level: Encryption::Level::None).save! original_settings = settings.settings_string @@ -176,7 +175,7 @@ module PlaceOS::Api result = client.patch( path: path, body: settings.to_json, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -189,17 +188,17 @@ module PlaceOS::Api end describe "scopes" do - Specs.test_controller_scope(Settings) + Spec.test_controller_scope(Settings) it "checks scope on update" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("settings", PlaceOS::Model::UserJWT::Scope::Access::Write)]) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("settings", PlaceOS::Model::UserJWT::Scope::Access::Write)]) settings = Model::Generator.settings(encryption_level: Encryption::Level::None).save! original_settings = settings.settings_string settings.settings_string = %(hello: "world"\n) id = settings.id.as(String) path = File.join(Settings.base_route, id) - result = Scopes.update(path, settings, scoped_authorization_header) + result = Scopes.update(path, settings, scoped_headers) result.status_code.should eq 200 updated = Model::Settings.from_trusted_json(result.body) @@ -208,8 +207,8 @@ module PlaceOS::Api updated.settings_string.should_not eq original_settings updated.destroy - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("settings", PlaceOS::Model::UserJWT::Scope::Access::Read)]) - result = Scopes.update(path, settings, scoped_authorization_header) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("settings", PlaceOS::Model::UserJWT::Scope::Access::Read)]) + result = Scopes.update(path, settings, scoped_headers) result.success?.should be_false result.status_code.should eq 403 diff --git a/spec/controllers/system-triggers_spec.cr b/spec/controllers/system-triggers_spec.cr index b1677f23..20e4eabc 100644 --- a/spec/controllers/system-triggers_spec.cr +++ b/spec/controllers/system-triggers_spec.cr @@ -3,12 +3,10 @@ require "timecop" module PlaceOS::Api describe SystemTriggers do - _, authorization_header = authentication - - Specs.test_404( + Spec.test_404( SystemTriggers.base_route.gsub(/:sys_id/, "sys-#{Random.rand(9999)}"), model_name: Model::TriggerInstance.table_name, - headers: authorization_header, + headers: Spec::Authentication.headers, ) describe "index", tags: "search" do @@ -33,7 +31,7 @@ module PlaceOS::Api params = HTTP::Params.encode({"as_of" => (inst1.updated_at.try &.to_unix).to_s}) path = "#{path}?#{params}" - correct_response = until_expected("GET", path, authorization_header) do |response| + correct_response = until_expected("GET", path, Spec::Authentication.headers) do |response| results = Array(Hash(String, JSON::Any)).from_json(response.body).map(&.["id"].as_s) contains_correct = results.any?(inst1.id) contains_incorrect = results.any?(inst2.id) @@ -57,7 +55,7 @@ module PlaceOS::Api result = client.post( path: path, body: body, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 201 @@ -73,7 +71,7 @@ module PlaceOS::Api id = trigger_instance.id.not_nil! path = SystemTriggers.base_route.gsub(/:sys_id/, sys.id) + id - result = client.get(path: path, headers: authorization_header) + result = client.get(path: path, headers: Spec::Authentication.headers) result.status_code.should eq 200 @@ -99,7 +97,7 @@ module PlaceOS::Api result = client.patch( path: path, body: {important: updated_importance}.to_json, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -121,7 +119,7 @@ module PlaceOS::Api id = model.id.not_nil! path = SystemTriggers.base_route.gsub(/:sys_id/, sys.id) + id - result = client.delete(path: path, headers: authorization_header) + result = client.delete(path: path, headers: Spec::Authentication.headers) result.status_code.should eq 200 Model::TriggerInstance.find(id.as(String)).should be_nil diff --git a/spec/controllers/systems_spec.cr b/spec/controllers/systems_spec.cr index 3050c6ca..d8314276 100644 --- a/spec/controllers/systems_spec.cr +++ b/spec/controllers/systems_spec.cr @@ -35,12 +35,10 @@ module PlaceOS::Api end describe Systems do - _, authorization_header = authentication - - Specs.test_404(Systems.base_route, model_name: Model::ControlSystem.table_name, headers: authorization_header) + Spec.test_404(Systems.base_route, model_name: Model::ControlSystem.table_name, headers: Spec::Authentication.headers) describe "index", tags: "search" do - Specs.test_base_index(klass: Model::ControlSystem, controller_klass: Systems) + Spec.test_base_index(klass: Model::ControlSystem, controller_klass: Systems) context "query parameter" do it "zone_id filters systems by zones" do @@ -69,7 +67,7 @@ module PlaceOS::Api path = "#{Systems.base_route}?#{params}" refresh_elastic(Model::ControlSystem.table_name) - found = until_expected("GET", path, authorization_header) do |response| + found = until_expected("GET", path, Spec::Authentication.headers) do |response| returned_ids = Array(Hash(String, JSON::Any)).from_json(response.body).map(&.["id"].as_s) (returned_ids | expected_ids).size == total_ids end @@ -96,7 +94,7 @@ module PlaceOS::Api params = HTTP::Params.encode({"email" => expected_emails.join(',')}) path = "#{Systems.base_route}?#{params}" - found = until_expected("GET", path, authorization_header) do |response| + found = until_expected("GET", path, Spec::Authentication.headers) do |response| refresh_elastic(Model::ControlSystem.table_name) returned_ids = Array(Hash(String, JSON::Any)).from_json(response.body).map(&.["id"].as_s) (returned_ids | expected_ids).size == total_ids @@ -129,7 +127,7 @@ module PlaceOS::Api params = HTTP::Params.encode({"module_id" => module_id}) path = "#{Systems.base_route}?#{params}" - found = until_expected("GET", path, authorization_header) do |response| + found = until_expected("GET", path, Spec::Authentication.headers) do |response| refresh_elastic(Model::ControlSystem.table_name) returned_ids = Array(Hash(String, JSON::Any)).from_json(response.body).map(&.["id"].as_s) (returned_ids | expected_ids).size == total_ids @@ -154,7 +152,7 @@ module PlaceOS::Api result = client.get( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -171,7 +169,7 @@ module PlaceOS::Api cs.persisted?.should be_true mod.persisted?.should be_true - spec_add_module(cs, mod, authorization_header) + spec_add_module(cs, mod, Spec::Authentication.headers) {cs, mod}.each &.destroy end @@ -183,7 +181,7 @@ module PlaceOS::Api result = client.put( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 404 @@ -200,13 +198,13 @@ module PlaceOS::Api cs2.persisted?.should be_true mod.persisted?.should be_true - cs1 = spec_add_module(cs1, mod, authorization_header) + cs1 = spec_add_module(cs1, mod, Spec::Authentication.headers) - spec_add_module(cs2, mod, authorization_header) + spec_add_module(cs2, mod, Spec::Authentication.headers) - cs1 = spec_delete_module(cs1, mod, authorization_header) + cs1 = spec_delete_module(cs1, mod, Spec::Authentication.headers) - spec_add_module(cs1, mod, authorization_header) + spec_add_module(cs1, mod, Spec::Authentication.headers) end end @@ -224,7 +222,7 @@ module PlaceOS::Api mod_id = mod.id.as(String) - spec_delete_module(cs, mod, authorization_header) + spec_delete_module(cs, mod, Spec::Authentication.headers) Model::Module.find(mod_id).should be_nil {mod, cs}.each &.try &.destroy @@ -246,7 +244,7 @@ module PlaceOS::Api cs1.modules.should contain mod_id cs2.modules.should contain mod_id - cs1 = spec_delete_module(cs1, mod, authorization_header) + cs1 = spec_delete_module(cs1, mod, Spec::Authentication.headers) cs2 = Model::ControlSystem.find!(cs2.id.as(String)) cs2.modules.should contain mod_id @@ -282,7 +280,7 @@ module PlaceOS::Api path = "#{Systems.base_route}#{control_system.id}/settings" result = client.get( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.success?.should be_true @@ -306,7 +304,7 @@ module PlaceOS::Api result = client.get( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) unless result.success? @@ -341,7 +339,7 @@ module PlaceOS::Api result = client.get( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.body.includes?("function1").should be_true @@ -372,7 +370,7 @@ module PlaceOS::Api result = client.get( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -406,7 +404,7 @@ module PlaceOS::Api response = client.get( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) String.from_json(response.body).should eq("1") end @@ -428,7 +426,7 @@ module PlaceOS::Api response = client.get( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) state = Hash(String, String).from_json(response.body) @@ -450,7 +448,7 @@ module PlaceOS::Api result = client.post( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -477,7 +475,7 @@ module PlaceOS::Api result = client.post( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -496,7 +494,7 @@ module PlaceOS::Api result = client.get( path: Systems.base_route + "#{system_id}/metadata", - headers: authorization_header, + headers: Spec::Authentication.headers, ) metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) @@ -510,7 +508,7 @@ module PlaceOS::Api end describe "CRUD operations", tags: "crud" do - Specs.test_crd(klass: Model::ControlSystem, controller_klass: Systems) + Spec.test_crd(klass: Model::ControlSystem, controller_klass: Systems) describe "update" do it "if version is valid" do @@ -528,7 +526,7 @@ module PlaceOS::Api result = client.patch( path: path, body: cs.to_json, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -548,7 +546,7 @@ module PlaceOS::Api result = client.patch( path: path, body: cs.to_json, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 409 @@ -564,7 +562,7 @@ module PlaceOS::Api result = client.get( path: Systems.base_route + "#{system_id}/metadata", - headers: authorization_header, + headers: Spec::Authentication.headers, ) metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) @@ -578,9 +576,9 @@ module PlaceOS::Api end describe "scopes" do - Specs.test_controller_scope(Systems) + Spec.test_controller_scope(Systems) it "should not allow start" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("systems", :read)]) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("systems", :read)]) cs = Model::Generator.control_system.save! mod = Model::Generator.module(control_system: cs).save! @@ -594,14 +592,14 @@ module PlaceOS::Api result = client.post( path: path, - headers: scoped_authorization_header, + headers: scoped_headers, ) result.status_code.should eq 403 end it "should allow start" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("systems", :write)]) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("systems", :write)]) cs = Model::Generator.control_system.save! mod = Model::Generator.module(control_system: cs).save! @@ -615,7 +613,7 @@ module PlaceOS::Api result = client.post( path: path, - headers: scoped_authorization_header, + headers: scoped_headers, ) result.status_code.should eq 200 diff --git a/spec/controllers/triggers_spec.cr b/spec/controllers/triggers_spec.cr index b8f80cab..3e7e296f 100644 --- a/spec/controllers/triggers_spec.cr +++ b/spec/controllers/triggers_spec.cr @@ -2,12 +2,10 @@ require "../helper" module PlaceOS::Api describe Triggers do - _authenticated_user, authorization_header = authentication - - Specs.test_404(Triggers.base_route, model_name: Model::Trigger.table_name, headers: authorization_header) + Spec.test_404(Triggers.base_route, model_name: Model::Trigger.table_name, headers: Spec::Authentication.headers) describe "index", tags: "search" do - Specs.test_base_index(klass: Model::Trigger, controller_klass: Triggers) + Spec.test_base_index(klass: Model::Trigger, controller_klass: Triggers) end describe "GET /triggers/:id/instances" do @@ -17,7 +15,7 @@ module PlaceOS::Api response = client.get( path: File.join(Triggers.base_route, trigger.id.not_nil!, "instances"), - headers: authorization_header, + headers: Spec::Authentication.headers, ) # Can't use from_json directly on the model as `id` will not be parsed @@ -29,7 +27,8 @@ module PlaceOS::Api end describe "CRUD operations", tags: "crud" do - Specs.test_crd(klass: Model::Trigger, controller_klass: Triggers) + Spec.test_crd(klass: Model::Trigger, controller_klass: Triggers) + it "update" do trigger = Model::Generator.trigger.save! original_name = trigger.name @@ -41,7 +40,7 @@ module PlaceOS::Api result = client.patch( path: path, body: trigger.to_json, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -63,7 +62,7 @@ module PlaceOS::Api result = client.get( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) response = JSON.parse(result.body) @@ -74,7 +73,7 @@ module PlaceOS::Api end describe "scopes" do - Specs.test_controller_scope(Triggers) - Specs.test_update_write_scope(Triggers) + Spec.test_controller_scope(Triggers) + Spec.test_update_write_scope(Triggers) end end diff --git a/spec/controllers/users_spec.cr b/spec/controllers/users_spec.cr index 12c77775..dc45cb8d 100644 --- a/spec/controllers/users_spec.cr +++ b/spec/controllers/users_spec.cr @@ -2,8 +2,7 @@ require "../helper" module PlaceOS::Api describe Users do - authenticated_user, authorization_header = authentication - Specs.test_404(Users.base_route, model_name: Model::User.table_name, headers: authorization_header) + Spec.test_404(Users.base_route, model_name: Model::User.table_name, headers: Spec::Authentication.headers) describe "CRUD operations", tags: "crud" do it "query via email" do @@ -18,7 +17,7 @@ module PlaceOS::Api result = client.get( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -33,7 +32,7 @@ module PlaceOS::Api id = model.id.as(String) result = client.get( path: File.join(Users.base_route, id), - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -49,7 +48,7 @@ module PlaceOS::Api id = model.id.as(String) result = client.get( path: Users.base_route + model.email.to_s, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -69,7 +68,7 @@ module PlaceOS::Api id = model.id.as(String) result = client.get( path: Users.base_route + login, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -88,7 +87,7 @@ module PlaceOS::Api id = model.id.as(String) result = client.get( path: Users.base_route + staff_id, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -113,7 +112,7 @@ module PlaceOS::Api result = client.patch( path: File.join(Users.base_route, id), body: {groups: updated_groups}.to_json, - headers: authorization_header + headers: Spec::Authentication.headers ) result.status_code.should eq 200 @@ -128,15 +127,14 @@ module PlaceOS::Api describe "GET /users/current" do it "renders the current user" do - authenticated_user, authorization_header = authentication result = client.get( path: File.join(Users.base_route, "/current"), - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 response_user = Model::User.from_trusted_json(result.body) - response_user.id.should eq authenticated_user.id + response_user.id.should eq Spec::Authentication.user.id end end @@ -148,7 +146,7 @@ module PlaceOS::Api result = client.get( path: Users.base_route + "#{user_id}/metadata", - headers: authorization_header, + headers: Spec::Authentication.headers, ) metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) @@ -162,7 +160,7 @@ module PlaceOS::Api end describe "scopes" do - Specs.test_controller_scope(Users) + Spec.test_controller_scope(Users) end end end diff --git a/spec/controllers/zones_spec.cr b/spec/controllers/zones_spec.cr index d534a6ea..4a59044b 100644 --- a/spec/controllers/zones_spec.cr +++ b/spec/controllers/zones_spec.cr @@ -2,16 +2,14 @@ require "../helper" module PlaceOS::Api describe Zones do - _authenticated_user, authorization_header = authentication - - Specs.test_404(Zones.base_route, model_name: Model::Zone.table_name, headers: authorization_header) + Spec.test_404(Zones.base_route, model_name: Model::Zone.table_name, headers: Spec::Authentication.headers) describe "index", tags: "search" do - Specs.test_base_index(klass: Model::Zone, controller_klass: Zones) + Spec.test_base_index(klass: Model::Zone, controller_klass: Zones) end describe "CRUD operations", tags: "crud" do - Specs.test_crd(klass: Model::Zone, controller_klass: Zones) + Spec.test_crd(klass: Model::Zone, controller_klass: Zones) it "update" do zone = Model::Generator.zone.save! original_name = zone.name @@ -22,7 +20,7 @@ module PlaceOS::Api result = client.patch( path: path, body: zone.to_json, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.success?.should be_true @@ -42,7 +40,7 @@ module PlaceOS::Api result = client.get( path: Zones.base_route + "#{zone_id}/metadata", - headers: authorization_header, + headers: Spec::Authentication.headers, ) metadata = Hash(String, Model::Metadata::Interface).from_json(result.body) @@ -56,8 +54,8 @@ module PlaceOS::Api end describe "scopes" do - Specs.test_controller_scope(Zones) - Specs.test_update_write_scope(Zones) + Spec.test_controller_scope(Zones) + Spec.test_update_write_scope(Zones) end end end diff --git a/spec/helper.cr b/spec/helper.cr index 6a80fc18..9644f4f1 100644 --- a/spec/helper.cr +++ b/spec/helper.cr @@ -11,9 +11,27 @@ require "./spec_helpers/*" include PlaceOS::Api::SpecClient +abstract class ActionController::Base + macro inherited + macro finished + {% begin %} + def self.base_route + NAMESPACE[0] + end + {% end %} + end + end +end + +module PlaceOS::Api + include Spec::Authentication +end + Spec.before_suite do Log.builder.bind("*", backend: PlaceOS::LogBackend::STDOUT, level: :trace) clear_tables + PlaceOS::Api::Spec::Authentication.authenticated.tap { |a| pp! a } + sleep 100.milliseconds end Spec.before_each do @@ -37,8 +55,10 @@ def clear_tables {% begin %} Promise.all( {% for t in { + PlaceOS::Model::ApiKey, PlaceOS::Model::Asset, PlaceOS::Model::AssetInstance, + PlaceOS::Model::Authority, PlaceOS::Model::ControlSystem, PlaceOS::Model::Driver, PlaceOS::Model::Module, @@ -46,65 +66,15 @@ def clear_tables PlaceOS::Model::Settings, PlaceOS::Model::Trigger, PlaceOS::Model::TriggerInstance, + PlaceOS::Model::User, PlaceOS::Model::Zone, } %} Promise.defer { {{t.id}}.clear }, {% end %} - ) + ).get {% end %} end -CREATION_LOCK = Mutex.new(protection: :reentrant) - -# Yield an authenticated user, and a header with X-API-Key set -def x_api_authentication(sys_admin : Bool = true, support : Bool = true, scope = [PlaceOS::Model::UserJWT::Scope::PUBLIC]) - CREATION_LOCK.synchronize do - user, _header = authentication(sys_admin, support, scope) - unless api_key = user.api_tokens.first? - api_key = PlaceOS::Model::ApiKey.new - api_key.user = user - api_key.name = user.name - api_key.save! - end - - authorization_header = HTTP::Headers{ - "X-API-Key" => api_key.x_api_key.not_nil!, - "Content-Type" => "application/json", - } - - {api_key.user, authorization_header} - end -end - -# Yield an authenticated user, and a header with Authorization bearer set -# This method is synchronised due to the redundant top-level calls. -def authentication(sys_admin : Bool = true, support : Bool = true, scope = [PlaceOS::Model::UserJWT::Scope::PUBLIC]) - CREATION_LOCK.synchronize do - authenticated_user = generate_auth_user(sys_admin, support, scope) - - headers = HTTP::Headers{ - "Authorization" => "Bearer #{PlaceOS::Model::Generator.jwt(authenticated_user, scope).encode}", - "Content-Type" => "application/json", - "Host" => "localhost", - } - - {authenticated_user, headers} - end -end - -def generate_auth_user(sys_admin, support, scopes) - scope_list = scopes.try &.join('-', &.to_s) - - test_user_email = PlaceOS::Model::Email.new("test-#{"admin-" if sys_admin}#{"supp" if support}-scope-#{scope_list}-rest-api@place.tech") - - PlaceOS::Model::User.where(email: test_user_email).first? || PlaceOS::Model::Generator.user.tap do |user| - user.email = test_user_email - user.sys_admin = sys_admin - user.support = support - user.save! - end -end - def until_expected(method, path, headers : HTTP::Headers, timeout : Time::Span = 3.seconds, &block : HTTP::Client::Response -> Bool) client = ActionController::SpecHelper.client channel = Channel(Bool).new @@ -138,9 +108,7 @@ def until_expected(method, path, headers : HTTP::Headers, timeout : Time::Span = rescue end - success = channel.receive? - channel.close - !!success + !!channel.receive?.tap { channel.close } end def random_name diff --git a/spec/spec_helpers/authentication.cr b/spec/spec_helpers/authentication.cr new file mode 100644 index 00000000..93e99372 --- /dev/null +++ b/spec/spec_helpers/authentication.cr @@ -0,0 +1,75 @@ +require "mutex" + +module PlaceOS::Api::Spec::Authentication + CREATION_LOCK = Mutex.new(protection: :reentrant) + + class_getter authenticated : Tuple(Model::User, HTTP::Headers) do + authentication + end + + class_getter user : Model::User do + CREATION_LOCK.synchronize do + authenticated.first + end + end + + class_getter headers : HTTP::Headers do + CREATION_LOCK.synchronize do + authenticated.last + end + end + + # Yield an authenticated user, and a header with X-API-Key set + def self.x_api_authentication(sys_admin : Bool = true, support : Bool = true, scope = [PlaceOS::Model::UserJWT::Scope::PUBLIC]) + CREATION_LOCK.synchronize do + user, headers = authentication(sys_admin, support, scope) + + email = user.email.to_s + + PlaceOS::Model::ApiKey.where(name: email).each &.destroy + + api_key = PlaceOS::Model::ApiKey.new(name: email) + api_key.user = user + api_key.x_api_key # Ensure key is present + api_key.save! + + headers.delete("Authorization") + + headers["X-API-Key"] = api_key.x_api_key.not_nil! + + {user, headers} + end + end + + # Yield an authenticated user, and a header with Authorization bearer set + # This method is synchronised due to the redundant top-level calls. + def self.authentication(sys_admin : Bool = true, support : Bool = true, scope = [PlaceOS::Model::UserJWT::Scope::PUBLIC]) + CREATION_LOCK.synchronize do + authenticated_user = generate_auth_user(sys_admin, support, scope) + + headers = HTTP::Headers{ + "Authorization" => "Bearer #{PlaceOS::Model::Generator.jwt(authenticated_user, scope).encode}", + "Content-Type" => "application/json", + "Host" => "localhost", + } + + {authenticated_user, headers} + end + end + + def self.generate_auth_user(sys_admin, support, scopes) + CREATION_LOCK.synchronize do + authority = PlaceOS::Model::Authority.find_by_domain("localhost") || PlaceOS::Model::Generator.authority.tap { |a| + a.domain = "localhost" + }.save! + + scope_list = scopes.try &.join('-', &.to_s) + test_user_email = PlaceOS::Model::Email.new("test-#{"admin-" if sys_admin}#{"supp-" if support}scope-#{scope_list}-rest-api@place.tech") + + PlaceOS::Model::User.where(email: test_user_email, authority_id: authority.id.as(String)).first? || PlaceOS::Model::Generator.user(authority, support: support, admin: sys_admin).tap do |user| + user.email = test_user_email + user.save! + end + end + end +end diff --git a/spec/spec_helpers/scopes.cr b/spec/spec_helpers/scopes.cr index c2ec13df..2a88f15a 100644 --- a/spec/spec_helpers/scopes.cr +++ b/spec/spec_helpers/scopes.cr @@ -3,40 +3,40 @@ require "../helper" module PlaceOS::Api::Scopes extend self - def show(base, id, scoped_authorization_header) + def show(base, id, scoped_headers) client.get( path: File.join(base, id), - headers: scoped_authorization_header, + headers: scoped_headers, ) end - def index(path, scoped_authorization_header) + def index(path, scoped_headers) client.get( path: path, - headers: scoped_authorization_header, + headers: scoped_headers, ) end - def create(path, body, scoped_authorization_header) + def create(path, body, scoped_headers) client.post( path: path, body: body, - headers: scoped_authorization_header, + headers: scoped_headers, ) end - def delete(base, id, scoped_authorization_header) + def delete(base, id, scoped_headers) client.delete( path: File.join(base, id), - headers: scoped_authorization_header, + headers: scoped_headers, ) end - def update(path, body, scoped_authorization_header) + def update(path, body, scoped_headers) client.patch( path: path, body: body.to_json, - headers: scoped_authorization_header, + headers: scoped_headers, ) end end diff --git a/spec/spec_helpers/specs.cr b/spec/spec_helpers/spec.cr similarity index 69% rename from spec/spec_helpers/specs.cr rename to spec/spec_helpers/spec.cr index 31d4d7aa..2019daf1 100644 --- a/spec/spec_helpers/specs.cr +++ b/spec/spec_helpers/spec.cr @@ -1,4 +1,6 @@ -module PlaceOS::Api::Specs +require "./authentication" + +module PlaceOS::Api::Spec # Check application responds with 404 when model not present def self.test_404(base, model_name, headers : HTTP::Headers) it "404s if #{model_name} isn't present in database", tags: "search" do @@ -14,7 +16,7 @@ module PlaceOS::Api::Specs {% klass_name = klass.stringify.split("::").last.underscore %} it "queries #{ {{ klass_name }} }", tags: "search" do - _, authorization_header = authentication + _, headers = Spec::Authentication.authentication doc = PlaceOS::Model::Generator.{{ klass_name.id }} name = random_name doc.name = name @@ -24,10 +26,9 @@ module PlaceOS::Api::Specs doc.persisted?.should be_true params = HTTP::Params.encode({"q" => name}) - path = "#{{{controller_klass}}::NAMESPACE[0].rstrip('/')}?#{params}" - header = authorization_header + path = "#{{{controller_klass}}.base_route.rstrip('/')}?#{params}" - found = until_expected("GET", path, header) do |response| + found = until_expected("GET", path, headers) do |response| Array(Hash(String, JSON::Any)) .from_json(response.body) .map(&.["id"].as_s) @@ -41,12 +42,11 @@ module PlaceOS::Api::Specs {% klass_name = klass.stringify.split("::").last.underscore %} it "create" do - _, authorization_header = authentication body = PlaceOS::Model::Generator.{{ klass_name.id }}.to_json result = client.post( {{ controller_klass }}.base_route, body: body, - headers: authorization_header + headers: Spec::Authentication.headers ) result.status_code.should eq 201 @@ -59,13 +59,12 @@ module PlaceOS::Api::Specs {% klass_name = klass.stringify.split("::").last.underscore %} it "show" do - _, authorization_header = authentication model = PlaceOS::Model::Generator.{{ klass_name.id }}.save! model.persisted?.should be_true id = model.id.as(String) result = client.get( path: File.join({{ controller_klass }}.base_route, id), - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.status_code.should eq 200 @@ -80,13 +79,12 @@ module PlaceOS::Api::Specs {% klass_name = klass.stringify.split("::").last.underscore %} it "destroy" do - _, authorization_header = authentication model = PlaceOS::Model::Generator.{{ klass_name.id }}.save! model.persisted?.should be_true id = model.id.as(String) result = client.delete( path: File.join({{ controller_klass }}.base_route, id), - headers: authorization_header, + headers: Spec::Authentication.headers ) result.status_code.should eq 200 @@ -95,9 +93,9 @@ module PlaceOS::Api::Specs end macro test_crd(klass, controller_klass) - Specs.test_create({{ klass }}, {{ controller_klass }}) - Specs.test_show({{ klass }}, {{ controller_klass }}) - Specs.test_destroy({{ klass }}, {{ controller_klass }}) + Spec.test_create({{ klass }}, {{ controller_klass }}) + Spec.test_show({{ klass }}, {{ controller_klass }}) + Spec.test_destroy({{ klass }}, {{ controller_klass }}) end macro test_controller_scope(klass) @@ -121,12 +119,12 @@ module PlaceOS::Api::Specs context "read" do it "allows access to show" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :read)]) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :read)]) model = PlaceOS::Model::Generator.{{ model_gen.id }}.save! model.persisted?.should be_true id = model.id.as(String) - result = Scopes.show({{ base }}, id, scoped_authorization_header) + result = Scopes.show({{ base }}, id, scoped_headers) result.status_code.should eq 200 response_model = Model::{{ model_name.id }}.from_trusted_json(result.body) response_model.id.should eq id @@ -134,27 +132,27 @@ module PlaceOS::Api::Specs end it "allows access to index" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :read)]) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :read)]) - result = Scopes.index({{ base }}, scoped_authorization_header) + result = Scopes.index({{ base }}, scoped_headers) result.success?.should be_true end it "should not allow access to create" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :read)]) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :read)]) body = PlaceOS::Model::Generator.{{ model_gen.id }}.to_json - result = Scopes.create({{ base }}, body, scoped_authorization_header) + result = Scopes.create({{ base }}, body, scoped_headers) result.status_code.should eq 403 end it "should not allow access to delete" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :read)]) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :read)]) model = PlaceOS::Model::Generator.{{ model_gen.id }}.save! model.persisted?.should be_true id = model.id.as(String) - result = Scopes.delete({{ base }}, id, scoped_authorization_header) + result = Scopes.delete({{ base }}, id, scoped_headers) result.status_code.should eq 403 Model::{{ model_name.id }}.find(id).should_not be_nil end @@ -162,28 +160,28 @@ module PlaceOS::Api::Specs context "write" do it "should not allow access to show" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :write)]) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :write)]) model = PlaceOS::Model::Generator.{{ model_gen.id }}.save! model.persisted?.should be_true id = model.id.as(String) - result = Scopes.show({{ base }}, id, scoped_authorization_header) + result = Scopes.show({{ base }}, id, scoped_headers) result.status_code.should eq 403 model.destroy end it "should not allow access to index" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :write)]) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :write)]) - result = Scopes.index({{ base }}, scoped_authorization_header) + result = Scopes.index({{ base }}, scoped_headers) result.status_code.should eq 403 end it "should allow access to create" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :write)]) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :write)]) body = PlaceOS::Model::Generator.{{ model_gen.id }}.to_json - result = Scopes.create({{ base }}, body, scoped_authorization_header) + result = Scopes.create({{ base }}, body, scoped_headers) result.success?.should be_true response_model = Model::{{ model_name.id }}.from_trusted_json(result.body) @@ -191,11 +189,11 @@ module PlaceOS::Api::Specs end it "should allow access to delete" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :write)]) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :write)]) model = PlaceOS::Model::Generator.{{ model_gen.id }}.save! model.persisted?.should be_true id = model.id.as(String) - result = Scopes.delete({{ base }}, id, scoped_authorization_header) + result = Scopes.delete({{ base }}, id, scoped_headers) result.success?.should be_true Model::{{ model_name.id }}.find(id).should be_nil end @@ -216,14 +214,14 @@ module PlaceOS::Api::Specs {% scope_name = klass.stringify.underscore %} it "checks scope on update" do - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, PlaceOS::Model::UserJWT::Scope::Access::Write)]) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, PlaceOS::Model::UserJWT::Scope::Access::Write)]) model = Model::Generator.{{ model_gen.id }}.save! original_name = model.name model.name = random_name id = model.id.as(String) path = File.join({{ base }}, id) - result = Scopes.update(path, model, scoped_authorization_header) + result = Scopes.update(path, model, scoped_headers) result.success?.should be_true updated = Model::{{ model_name.id }}.from_trusted_json(result.body) @@ -232,8 +230,8 @@ module PlaceOS::Api::Specs updated.name.should_not eq original_name updated.destroy - _, scoped_authorization_header = authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, PlaceOS::Model::UserJWT::Scope::Access::Read)]) - result = Scopes.update(path, model, scoped_authorization_header) + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, PlaceOS::Model::UserJWT::Scope::Access::Read)]) + result = Scopes.update(path, model, scoped_headers) result.success?.should be_false result.status_code.should eq 403 diff --git a/spec/websocket/session_spec.cr b/spec/websocket/session_spec.cr index 070c33c0..7396fbe6 100644 --- a/spec/websocket/session_spec.cr +++ b/spec/websocket/session_spec.cr @@ -4,12 +4,10 @@ require "webmock" require "../helper" module PlaceOS::Api::WebSocket - authenticated_user, authorization_header = authentication - pending Session do describe "systems/control" do it "opens a websocket session" do - bind(Systems.base_route, authorization_header) do |ws| + bind(Systems.base_route, Spec::Authentication.headers) do |ws| ws.closed?.should be_false end end @@ -20,7 +18,7 @@ module PlaceOS::Api::WebSocket it "receives updates" do # Status to bind status_name = "nugget" - results = test_websocket_api(Systems.base_route, authorization_header) do |ws, control_system, mod| + results = test_websocket_api(Systems.base_route, Spec::Authentication.headers) do |ws, control_system, mod| # Create a storage proxy driver_proxy = PlaceOS::Driver::RedisStorage.new mod.id.as(String) @@ -63,7 +61,7 @@ module PlaceOS::Api::WebSocket status_name = "nugget" id = rand(10).to_i64 - results = test_websocket_api(Systems.base_route, authorization_header) do |ws, control_system, mod| + results = test_websocket_api(Systems.base_route, Spec::Authentication.headers) do |ws, control_system, mod| request = { id: id, system_id: control_system.id.as(String), @@ -97,7 +95,7 @@ module PlaceOS::Api::WebSocket status_name = "function2" id = rand(10).to_i64 - updates, _, _ = test_websocket_exec(Systems.base_route, authorization_header) do |ws, control_system, mod| + updates, _, _ = test_websocket_exec(Systems.base_route, Spec::Authentication.headers) do |ws, control_system, mod| request = { id: id, system_id: control_system.id.as(String), @@ -119,7 +117,7 @@ module PlaceOS::Api::WebSocket status_name = "nugget" id = rand(10).to_i64 - updates, _, _ = test_websocket_api(Systems.base_route, authorization_header) do |ws, control_system, mod| + updates, _, _ = test_websocket_api(Systems.base_route, Spec::Authentication.headers) do |ws, control_system, mod| request = { id: id, system_id: control_system.id.as(String), @@ -141,7 +139,7 @@ module PlaceOS::Api::WebSocket status_name = "nugget" id = rand(10).to_i64 - updates, _, _ = test_websocket_api(Systems.base_route, authorization_header) do |ws, control_system, mod| + updates, _, _ = test_websocket_api(Systems.base_route, Spec::Authentication.headers) do |ws, control_system, mod| request = { id: id, system_id: control_system.id.as(String), @@ -214,7 +212,7 @@ end # Binds to the websocket API # Yields API websocket, and a control system + module # Cleans up the websocket and models -def test_websocket_api(base, authorization_header) +def test_websocket_api(base, headers) # Create a System control_system = PlaceOS::Model::Generator.control_system.save! @@ -231,7 +229,7 @@ def test_websocket_api(base, authorization_header) lookup_key = "#{mod.custom_name}/1" sys_lookup[lookup_key] = mod.id.as(String) - bind(base, authorization_header, on_message) do |ws| + bind(base, headers, on_message) do |ws| yield ({ws, control_system, mod}) end @@ -242,7 +240,7 @@ def test_websocket_api(base, authorization_header) {updates, control_system, mod} end -def test_websocket_exec(base, authorization_header) +def test_websocket_exec(base, headers) control_system = PlaceOS::Model::Generator.control_system.save! mod = PlaceOS::Model::Generator.module(control_system: control_system).save! @@ -268,7 +266,7 @@ def test_websocket_exec(base, authorization_header) updates << PlaceOS::Api::WebSocket::Session::Response.from_json message } - bind(base, authorization_header, on_message) do |ws| + bind(base, headers, on_message) do |ws| yield ({ws, control_system, mod}) end diff --git a/src/constants.cr b/src/constants.cr index 2e5c1f4c..a4ca9395 100644 --- a/src/constants.cr +++ b/src/constants.cr @@ -45,7 +45,7 @@ module PlaceOS::Api private PLACE_TAG_PREFIX = "placeos-" private BUILD_CHANGELOG = {{ !PLATFORM_VERSION.downcase.starts_with?("dev") }} - PLATFORM_CHANGELOG = fetch_platform_changelog(BUILD_CHANGELOG) + PLATFORM_CHANGELOG = "" # fetch_platform_changelog(BUILD_CHANGELOG) macro fetch_platform_changelog(build) {% if build %} diff --git a/test b/test index 338e043d..ef5e5aec 100755 --- a/test +++ b/test @@ -13,7 +13,7 @@ function trap_ctrlc () # when signal 2 (SIGINT) is received trap "trap_ctrlc" 2 -docker-compose pull -q +# docker-compose pull -q exit_code="0" From 45e53c4aed9fa108d7a1835efc8451bc34a1f80a Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Mon, 27 Jun 2022 23:10:07 +1000 Subject: [PATCH 23/29] test: fiddling with spec execution order to fix auth errors --- spec/controllers/metadata_spec.cr | 2 +- spec/helper.cr | 7 +++---- spec/spec_helpers/authentication.cr | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/spec/controllers/metadata_spec.cr b/spec/controllers/metadata_spec.cr index 5810af0d..9a5ee83a 100644 --- a/spec/controllers/metadata_spec.cr +++ b/spec/controllers/metadata_spec.cr @@ -59,7 +59,7 @@ module PlaceOS::Api end describe "PUT /metadata" do - it "creates metadata", focus: true do + it "creates metadata" do parent = Model::Generator.zone.save! meta = Model::Metadata::Interface.new( diff --git a/spec/helper.cr b/spec/helper.cr index 9644f4f1..076b335e 100644 --- a/spec/helper.cr +++ b/spec/helper.cr @@ -30,16 +30,15 @@ end Spec.before_suite do Log.builder.bind("*", backend: PlaceOS::LogBackend::STDOUT, level: :trace) clear_tables - PlaceOS::Api::Spec::Authentication.authenticated.tap { |a| pp! a } - sleep 100.milliseconds + PlaceOS::Api::Spec::Authentication.authenticated end +Spec.after_suite { clear_tables } + Spec.before_each do PlaceOS::Api::HttpMocks.reset end -Spec.after_suite { clear_tables } - # Application config require "../src/config" diff --git a/spec/spec_helpers/authentication.cr b/spec/spec_helpers/authentication.cr index 93e99372..91f44c28 100644 --- a/spec/spec_helpers/authentication.cr +++ b/spec/spec_helpers/authentication.cr @@ -3,17 +3,17 @@ require "mutex" module PlaceOS::Api::Spec::Authentication CREATION_LOCK = Mutex.new(protection: :reentrant) - class_getter authenticated : Tuple(Model::User, HTTP::Headers) do + def self.authenticated : Tuple(Model::User, HTTP::Headers) authentication end - class_getter user : Model::User do + def self.user : Model::User CREATION_LOCK.synchronize do authenticated.first end end - class_getter headers : HTTP::Headers do + def self.headers : HTTP::Headers CREATION_LOCK.synchronize do authenticated.last end From 25350acd27e4b53f423c0cbce3157e510f96b849 Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Tue, 28 Jun 2022 01:54:06 +1000 Subject: [PATCH 24/29] test: chipping away --- spec/controllers/api_key_spec.cr | 7 ++++--- spec/controllers/modules_spec.cr | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/spec/controllers/api_key_spec.cr b/spec/controllers/api_key_spec.cr index c5fb0f15..16d76f8b 100644 --- a/spec/controllers/api_key_spec.cr +++ b/spec/controllers/api_key_spec.cr @@ -1,10 +1,11 @@ require "../helper" module PlaceOS::Api - _, scoped_headers = Spec::Authentication.x_api_authentication - describe ApiKeys do - Spec.test_404(ApiKeys.base_route, model_name: Model::ApiKey.table_name, headers: scoped_headers) + _, scoped_headers = Spec::Authentication.x_api_authentication + before_all { _, scoped_headers = Spec::Authentication.x_api_authentication } + + Spec.test_404(ApiKeys.base_route, model_name: Model::ApiKey.table_name, headers: Spec::Authentication.headers) describe "index", tags: "search" do Spec.test_base_index(Model::ApiKey, ApiKeys) diff --git a/spec/controllers/modules_spec.cr b/spec/controllers/modules_spec.cr index 0adb6fc3..52aeb6dd 100644 --- a/spec/controllers/modules_spec.cr +++ b/spec/controllers/modules_spec.cr @@ -86,10 +86,11 @@ module PlaceOS::Api # Call the index method of the controller response = client.get( - "#{Modules.base_route}?#{HTTP::Params{"control_system_id" => sys.id.as(String)}}" + "#{Modules.base_route}?#{HTTP::Params{"control_system_id" => sys.id.as(String)}}", + headers: Spec::Authentication.headers, ) - response.status.success?.should be_true + response.status_code.should eq 200 response.headers["X-Total-Count"].should eq("1") Array(Hash(String, JSON::Any)).from_json(response.body.to_s).map(&.["id"].as_s).first?.should eq(mod.id) end From 47ca0456befac0b59f03c941d5ec46baf99e9bf2 Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Wed, 29 Jun 2022 22:51:49 +1000 Subject: [PATCH 25/29] wip --- shard.lock | 2 +- spec/controllers/edges_spec.cr | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/shard.lock b/shard.lock index bac48b26..7737a017 100644 --- a/shard.lock +++ b/shard.lock @@ -215,7 +215,7 @@ shards: placeos-core: git: https://github.com/placeos/core.git - version: 4.3.1+git.commit.cfc21ce9f33405a84a5659fc688b48dd7c077178 + version: 4.3.1+git.commit.ea85300c41bd8698223bc847fc02ac4dae855e12 placeos-core-client: # Overridden git: https://github.com/placeos/core-client.git diff --git a/spec/controllers/edges_spec.cr b/spec/controllers/edges_spec.cr index fac6c384..a6b80028 100644 --- a/spec/controllers/edges_spec.cr +++ b/spec/controllers/edges_spec.cr @@ -10,7 +10,7 @@ module PlaceOS::Api end describe "/control" do - it "authenticates with an API key from a new edge" do + it "authenticates with an API key from a new edge", focus: true do # Create a new edge to test with as the controller would edge_host = "localhost" @@ -32,7 +32,7 @@ module PlaceOS::Api secret: new_edge.x_api_key ) - websocket = client.establish_ws(path, headers: HTTP::Headers{"Host" => edge_host}) + websocket = client.establish_ws(uri, headers: HTTP::Headers{"Host" => edge_host}) edge_client.connect(websocket) do edge_client.transport.closed?.should be_false edge_client.disconnect From 12698c07e3bbd2adc2445ca8a3df6f3a7f3e5af7 Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Thu, 14 Jul 2022 15:40:56 +1000 Subject: [PATCH 26/29] fix: majority of specs fixed --- docker-compose.yml | 19 ++---------------- shard.lock | 8 ++++---- spec/controllers/drivers_spec.cr | 3 ++- spec/controllers/edges_spec.cr | 5 +++-- spec/controllers/systems_spec.cr | 3 ++- spec/websocket/session_spec.cr | 22 +++++---------------- src/placeos-rest-api/controllers/systems.cr | 7 +++++-- 7 files changed, 23 insertions(+), 44 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7b667fe4..85bd1aea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,7 +32,7 @@ x-search-ingest-client-env: &search-ingest-client-env services: test: # Rest API - image: placeos/service-spec-runner:${CRYSTAL_VERSION:-1.4.1} + image: placeos/service-spec-runner:${CRYSTAL_VERSION:-1.5.0} volumes: - ${PWD}/spec:/app/spec - ${PWD}/src:/app/src @@ -42,7 +42,6 @@ services: - ${PWD}/shard.yml:/app/shard.yml - ${PWD}/coverage:/app/coverage depends_on: - # - auth - build - core - elastic @@ -117,22 +116,8 @@ services: # Environment <<: *deployment-env - # auth: # Authentication Service - # image: placeos/auth:nightly - # restart: always - # hostname: auth - # depends_on: - # - redis - # - rethink - # environment: - # <<: *rethinkdb-client-env - # <<: *redis-client-env - # COAUTH_NO_SSL: "true" - # TZ: $TZ - # PLACE_URI: https://${PLACE_DOMAIN:-localhost:8443} - core: # Module coordinator - image: placeos/core:feat-build + image: ghcr.io/placeos/core:feat-build restart: always hostname: core depends_on: diff --git a/shard.lock b/shard.lock index 4415ad4b..1cd89b42 100644 --- a/shard.lock +++ b/shard.lock @@ -7,7 +7,7 @@ shards: action-controller: # Overridden git: https://github.com/spider-gazelle/action-controller.git - version: 5.1.4 + version: 5.1.6 active-model: git: https://github.com/spider-gazelle/active-model.git @@ -183,7 +183,7 @@ shards: openssl_ext: git: https://github.com/spider-gazelle/openssl_ext.git - version: 2.1.5 + version: 2.2.0 opentelemetry-api: git: https://github.com/wyhaines/opentelemetry-api.cr.git @@ -191,7 +191,7 @@ shards: opentelemetry-instrumentation: git: https://github.com/wyhaines/opentelemetry-instrumentation.cr.git - version: 0.3.6+git.commit.2b4477f57da6b593469deadc3a439e5ed83dd23f + version: 0.5.0+git.commit.7d83b9a53c9540fbc159f06eeed5d4bdad5c4377 opentelemetry-sdk: # Overridden git: https://github.com/wyhaines/opentelemetry-sdk.cr.git @@ -227,7 +227,7 @@ shards: placeos-driver: git: https://github.com/placeos/driver.git - version: 6.3.11 + version: 6.4.2 placeos-frontend-loader: git: https://github.com/placeos/frontend-loader.git diff --git a/spec/controllers/drivers_spec.cr b/spec/controllers/drivers_spec.cr index 24fd1a3b..a1e5f614 100644 --- a/spec/controllers/drivers_spec.cr +++ b/spec/controllers/drivers_spec.cr @@ -75,7 +75,8 @@ module PlaceOS::Api pending "GET /:id/compiled" do driver = get_driver - Utils::Changefeeds.await_model_change(driver, timeout: 90.seconds) do |update| + + Utils::Changefeeds.await_model_change(driver, timeout: 20.seconds) do |update| update.destroyed? || !update.recompile_commit? end diff --git a/spec/controllers/edges_spec.cr b/spec/controllers/edges_spec.cr index a6b80028..1b7eab26 100644 --- a/spec/controllers/edges_spec.cr +++ b/spec/controllers/edges_spec.cr @@ -10,7 +10,7 @@ module PlaceOS::Api end describe "/control" do - it "authenticates with an API key from a new edge", focus: true do + it "authenticates with an API key from a new edge" do # Create a new edge to test with as the controller would edge_host = "localhost" @@ -23,7 +23,7 @@ module PlaceOS::Api new_edge.x_api_key new_edge.save! - path = "#{Edges.base_route}/control" + path = File.join(Edges.base_route, "control") uri = URI.new(host: edge_host, path: path, query: URI::Params{"api-key" => new_edge.x_api_key}) @@ -33,6 +33,7 @@ module PlaceOS::Api ) websocket = client.establish_ws(uri, headers: HTTP::Headers{"Host" => edge_host}) + spawn(same_thread: true) { websocket.run } edge_client.connect(websocket) do edge_client.transport.closed?.should be_false edge_client.disconnect diff --git a/spec/controllers/systems_spec.cr b/spec/controllers/systems_spec.cr index d8314276..fe6cd7b2 100644 --- a/spec/controllers/systems_spec.cr +++ b/spec/controllers/systems_spec.cr @@ -406,7 +406,8 @@ module PlaceOS::Api path: path, headers: Spec::Authentication.headers, ) - String.from_json(response.body).should eq("1") + + Int32.from_json(response.body).should eq(1) end it "GET /systems/:sys_id/:module_slug" do diff --git a/spec/websocket/session_spec.cr b/spec/websocket/session_spec.cr index 7396fbe6..91fc982e 100644 --- a/spec/websocket/session_spec.cr +++ b/spec/websocket/session_spec.cr @@ -4,7 +4,7 @@ require "webmock" require "../helper" module PlaceOS::Api::WebSocket - pending Session do + describe Session do describe "systems/control" do it "opens a websocket session" do bind(Systems.base_route, Spec::Authentication.headers) do |ws| @@ -178,28 +178,16 @@ module PlaceOS::Api::WebSocket end end -# Generate a controller context for testing a websocket -# -def websocket_context(path) - context( - method: "GET", - path: path, - headers: HTTP::Headers{ - "Connection" => "Upgrade", - "Upgrade" => "websocket", - "Origin" => "localhost", - } - ) -end - # Binds to the system websocket endpoint # def bind(base, auth, on_message : Proc(String, _) = ->(_msg : String) {}) + host = "localhost" bearer = auth["Authorization"].split(' ').last - path = "#{base}control?bearer_token=#{bearer}" + path = File.join(base, "control?bearer_token=#{bearer}") # Create a websocket connection, then run the session - socket = HTTP::WebSocket.new("localhost", path, 6000) + socket = client.establish_ws(path, headers: HTTP::Headers{"Host" => host}) + socket.on_message &on_message spawn(same_thread: true) { socket.run } diff --git a/src/placeos-rest-api/controllers/systems.cr b/src/placeos-rest-api/controllers/systems.cr index 78387306..5bb10aac 100644 --- a/src/placeos-rest-api/controllers/systems.cr +++ b/src/placeos-rest-api/controllers/systems.cr @@ -188,8 +188,11 @@ module PlaceOS::Api def show # Guest JWTs include the control system id that they have access to if user_token.guest_scope? - head :forbidden unless user_token.user.roles.includes?(current_control_system.id) - render json: current_control_system + if user_token.user.roles.includes?(current_control_system.id) + render json: current_control_system + else + head :forbidden + end end render json: !complete? ? current_control_system : with_fields(current_control_system, { From 30fa51b08a2e0a777164188d63d1951761fc0991 Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Fri, 15 Jul 2022 12:59:23 +1000 Subject: [PATCH 27/29] fix: compiled test --- docker-compose.yml | 1 + shard.lock | 4 ++-- spec/controllers/drivers_spec.cr | 4 ++-- src/placeos-rest-api/controllers/triggers.cr | 1 + 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 85bd1aea..c59c148b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,7 @@ version: "3.7" # YAML Anchors x-deployment-env: &deployment-env + LOG_LEVEL: trace ENV: ${ENV:-development} SG_ENV: ${SG_ENV:-development} TZ: $TZ diff --git a/shard.lock b/shard.lock index 1cd89b42..d4a02dae 100644 --- a/shard.lock +++ b/shard.lock @@ -211,7 +211,7 @@ shards: placeos-build: git: https://github.com/placeos/build.git - version: 1.0.3+git.commit.c90103811698bde356f0ce8756768c91533b994c + version: 1.0.6+git.commit.f8d65b6ed947b1c28378214a9df54261bff2bd76 placeos-compiler: git: https://github.com/placeos/compiler.git @@ -227,7 +227,7 @@ shards: placeos-driver: git: https://github.com/placeos/driver.git - version: 6.4.2 + version: 6.4.4 placeos-frontend-loader: git: https://github.com/placeos/frontend-loader.git diff --git a/spec/controllers/drivers_spec.cr b/spec/controllers/drivers_spec.cr index a1e5f614..75088ef1 100644 --- a/spec/controllers/drivers_spec.cr +++ b/spec/controllers/drivers_spec.cr @@ -73,7 +73,7 @@ module PlaceOS::Api end end - pending "GET /:id/compiled" do + it "GET /:id/compiled" do driver = get_driver Utils::Changefeeds.await_model_change(driver, timeout: 20.seconds) do |update| @@ -88,7 +88,7 @@ module PlaceOS::Api response.success?.should be_true end - pending "POST /:id/recompile" do + it "POST /:id/recompile" do driver = get_driver response = client.post( diff --git a/src/placeos-rest-api/controllers/triggers.cr b/src/placeos-rest-api/controllers/triggers.cr index 84843ad7..6800fa7f 100644 --- a/src/placeos-rest-api/controllers/triggers.cr +++ b/src/placeos-rest-api/controllers/triggers.cr @@ -41,6 +41,7 @@ module PlaceOS::Api def show include_instances = boolean_param("instances") + render json: !include_instances ? current_trigger : with_fields(current_trigger, { :trigger_instances => current_trigger.trigger_instances.to_a, }) From 5e18bae2037b632f1f7998a786d4ea8b71660ef5 Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Fri, 15 Jul 2022 16:31:21 +1000 Subject: [PATCH 28/29] test: enable commit listing specs --- spec/controllers/asset_spec.cr | 2 +- spec/controllers/drivers_spec.cr | 7 ++++--- spec/controllers/modules_spec.cr | 10 +++++----- spec/controllers/repositories_spec.cr | 20 ++++++++++++++------ spec/controllers/systems_spec.cr | 4 ++-- spec/controllers/triggers_spec.cr | 2 +- 6 files changed, 27 insertions(+), 18 deletions(-) diff --git a/spec/controllers/asset_spec.cr b/spec/controllers/asset_spec.cr index 5487eca3..db815914 100644 --- a/spec/controllers/asset_spec.cr +++ b/spec/controllers/asset_spec.cr @@ -58,7 +58,7 @@ module PlaceOS::Api asset_instance_id = asset_instance.id.as(String) params = HTTP::Params{"instances" => "true"} - path = "#{Assets.base_route}#{asset.id}?#{params}" + path = File.join(Assets.base_route, "#{asset.id}?#{params}") result = client.get( path: path, diff --git a/spec/controllers/drivers_spec.cr b/spec/controllers/drivers_spec.cr index 75088ef1..0644d2e6 100644 --- a/spec/controllers/drivers_spec.cr +++ b/spec/controllers/drivers_spec.cr @@ -80,8 +80,9 @@ module PlaceOS::Api update.destroyed? || !update.recompile_commit? end + path = File.join(Drivers.base_route, "#{driver.id.not_nil!}/compiled") response = client.get( - path: "#{Drivers.base_route}#{driver.id.not_nil!}/compiled", + path: path, headers: Spec::Authentication.headers, ) @@ -90,9 +91,9 @@ module PlaceOS::Api it "POST /:id/recompile" do driver = get_driver - + path = File.join(Drivers.base_route, "#{driver.id.not_nil!}/recompile") response = client.post( - path: "#{Drivers.base_route}#{driver.id.not_nil!}/recompile", + path: path, headers: Spec::Authentication.headers, ) diff --git a/spec/controllers/modules_spec.cr b/spec/controllers/modules_spec.cr index 52aeb6dd..a6ce10f9 100644 --- a/spec/controllers/modules_spec.cr +++ b/spec/controllers/modules_spec.cr @@ -195,7 +195,7 @@ module PlaceOS::Api driver.settings, ].flat_map(&.compact_map(&.id)).reverse! - path = "#{Modules.base_route}#{mod.id}/settings" + path = File.join(Modules.base_route, "#{mod.id}/settings") result = client.get( path: path, headers: Spec::Authentication.headers, @@ -221,7 +221,7 @@ module PlaceOS::Api control_system.update! mod = Model::Generator.module(driver: driver, control_system: control_system).save! - path = "#{Modules.base_route}#{mod.id}/settings" + path = File.join(Modules.base_route, "#{mod.id}/settings") result = client.get( path: path, @@ -239,7 +239,7 @@ module PlaceOS::Api it "returns an empty array for a module without associated settings" do driver = Model::Generator.driver(role: Model::Driver::Role::Service).save! mod = Model::Generator.module(driver: driver).save! - path = "#{Modules.base_route}#{mod.id}/settings" + path = File.join(Modules.base_route, "#{mod.id}/settings") result = client.get( path: path, @@ -258,7 +258,7 @@ module PlaceOS::Api it "fails for logic module" do driver = Model::Generator.driver(role: Model::Driver::Role::Logic) mod = Model::Generator.module(driver: driver).save! - path = "#{Modules.base_route}#{mod.id}/ping" + path = File.join(Modules.base_route, "#{mod.id}/ping") result = client.post( path: path, headers: Spec::Authentication.headers, @@ -276,7 +276,7 @@ module PlaceOS::Api mod.ip = "127.0.0.1" mod.save! - path = "#{Modules.base_route}#{mod.id}/ping" + path = File.join(Modules.base_route, "#{mod.id}/ping") result = client.post( path: path, headers: Spec::Authentication.headers, diff --git a/spec/controllers/repositories_spec.cr b/spec/controllers/repositories_spec.cr index 89e1ec42..a58ec8fe 100644 --- a/spec/controllers/repositories_spec.cr +++ b/spec/controllers/repositories_spec.cr @@ -69,7 +69,7 @@ module PlaceOS::Api it "errors if enumerating drivers in an interface repo" do id = repo.id.as(String) - path = "#{Repositories.base_route}#{id}/drivers" + path = File.join(Repositories.base_route, "#{id}/drivers") result = client.get( path: path, headers: Spec::Authentication.headers, @@ -80,7 +80,7 @@ module PlaceOS::Api it "errors when requesting driver details from an interface repo" do id = repo.id.as(String) - path = "#{Repositories.base_route}#{id}/details" + path = File.join(Repositories.base_route, "#{id}/details") result = client.get( path: path, headers: Spec::Authentication.headers, @@ -106,16 +106,24 @@ module PlaceOS::Api repo.save! end - pending "fetches commits for a repository" do + it "fetches commits for a repository" do id = repo.id.as(String) - response = client.get("#{Repositories.base_route}#{id}/commits?#{HTTP::Params{"id" => id}}") + path = File.join(Repositories.base_route, "#{id}/commits?#{HTTP::Params{"id" => id}}") + response = client.get( + path: path, + headers: Spec::Authentication.headers, + ) response.status.should eq HTTP::Status::OK Array(String).from_json(response.body).should_not be_empty end - pending "fetches commits for a file" do + it "fetches commits for a file" do id = repo.id.as(String) - response = client.get("#{Repositories.base_route}#{id}/commits?#{HTTP::Params{"driver" => "drivers/place/private_helper.cr", "id" => id}}") + path = File.join(Repositories.base_route, "#{id}/commits?#{HTTP::Params{"driver" => "drivers/place/private_helper.cr", "id" => id}}") + response = client.get( + path: path, + headers: Spec::Authentication.headers, + ) response.status.should eq HTTP::Status::OK Array(String).from_json(response.body).should_not be_empty end diff --git a/spec/controllers/systems_spec.cr b/spec/controllers/systems_spec.cr index fe6cd7b2..cb80b1a0 100644 --- a/spec/controllers/systems_spec.cr +++ b/spec/controllers/systems_spec.cr @@ -277,7 +277,7 @@ module PlaceOS::Api zone0.settings, ].flat_map(&.compact_map(&.id)).reverse! - path = "#{Systems.base_route}#{control_system.id}/settings" + path = File.join(Systems.base_route, "#{control_system.id}/settings") result = client.get( path: path, headers: Spec::Authentication.headers, @@ -300,7 +300,7 @@ module PlaceOS::Api control_system.zones = [zone0.id.as(String), zone1.id.as(String)] control_system.save! - path = "#{Systems.base_route}#{control_system.id}/settings" + path = File.join(Systems.base_route, "#{control_system.id}/settings") result = client.get( path: path, diff --git a/spec/controllers/triggers_spec.cr b/spec/controllers/triggers_spec.cr index 3e7e296f..8a975c0e 100644 --- a/spec/controllers/triggers_spec.cr +++ b/spec/controllers/triggers_spec.cr @@ -58,7 +58,7 @@ module PlaceOS::Api trigger_instance_id = trigger_instance.id.as(String) params = HTTP::Params{"instances" => "true"} - path = "#{Triggers.base_route}#{trigger.id}?#{params}" + path = File.join(Triggers.base_route, "#{trigger.id}?#{params}") result = client.get( path: path, From 31dd4b02405d0f9fd2613a8cab69eee4f9877c06 Mon Sep 17 00:00:00 2001 From: Caspian Baska Date: Wed, 20 Jul 2022 17:00:33 +1000 Subject: [PATCH 29/29] feat: use build for commit listing --- spec/controllers/repositories_spec.cr | 12 +++++++----- .../controllers/repositories.cr | 18 +++++++++++++++--- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/spec/controllers/repositories_spec.cr b/spec/controllers/repositories_spec.cr index a58ec8fe..06b07263 100644 --- a/spec/controllers/repositories_spec.cr +++ b/spec/controllers/repositories_spec.cr @@ -98,32 +98,34 @@ module PlaceOS::Api end context "driver" do - repo = Model::Generator.repository(type: :driver).tap do |r| - r.uri = "https://github.com/placeOS/private-drivers" - end + repo = Model::Generator.repository(type: :driver) before_all do + repo.uri = "https://github.com/placeOS/private-drivers" repo.save! end it "fetches commits for a repository" do id = repo.id.as(String) - path = File.join(Repositories.base_route, "#{id}/commits?#{HTTP::Params{"id" => id}}") + path = File.join(Repositories.base_route, "#{id}/commits") response = client.get( path: path, headers: Spec::Authentication.headers, ) + response.status.should eq HTTP::Status::OK Array(String).from_json(response.body).should_not be_empty end it "fetches commits for a file" do id = repo.id.as(String) - path = File.join(Repositories.base_route, "#{id}/commits?#{HTTP::Params{"driver" => "drivers/place/private_helper.cr", "id" => id}}") + params = HTTP::Params{"driver" => "drivers/place/private_helper.cr"} + path = File.join(Repositories.base_route, "#{id}/commits?#{params}") response = client.get( path: path, headers: Spec::Authentication.headers, ) + response.status.should eq HTTP::Status::OK Array(String).from_json(response.body).should_not be_empty end diff --git a/src/placeos-rest-api/controllers/repositories.cr b/src/placeos-rest-api/controllers/repositories.cr index 6c67a3b4..27454d2e 100644 --- a/src/placeos-rest-api/controllers/repositories.cr +++ b/src/placeos-rest-api/controllers/repositories.cr @@ -151,9 +151,21 @@ module PlaceOS::Api number_of_commits = 50 if number_of_commits.nil? case repository.repo_type in .driver? - # Dial the core responsible for the driver - Api::Systems.core_for(repository.folder_name, request_id) do |core_client| - core_client.driver(file_name || ".", repository.folder_name, repository.branch, number_of_commits) + Build::Client.client do |build_client| + args = { + url: repository.uri, + branch: repository.branch, + username: repository.username, + password: repository.decrypt_password, + request_id: request_id, + count: number_of_commits, + } + + if file_name + build_client.file_commits(**args.merge(file: file_name)) + else + build_client.repository_commits(**args) + end.map(&.hash) # Extract only the commit hash from the commit object end in .interface? # Dial the frontends service