diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33d5193a..1f20da15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,8 @@ jobs: with: todo_issues: true first_commit: 0a1e7680dc203f278f18fbe1f81bfd2713b83d1c + secrets: + CR_PAT: ${{ secrets.GHCR_PAT }} crystal-style: uses: PlaceOS/.github/.github/workflows/crystal-style.yml@main diff --git a/docker-compose.yml b/docker-compose.yml index 6afcceab..c59c148b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,10 +3,15 @@ version: "3.7" # YAML Anchors x-deployment-env: &deployment-env + LOG_LEVEL: trace ENV: ${ENV:-development} 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 ELASTIC_HOST: ${ELASTIC_HOST:-elastic} ELASTIC_PORT: ${ELASTIC_PORT:-9200} @@ -15,7 +20,6 @@ x-etcd-client-env: &etcd-client-env ETCD_HOST: ${ETCD_HOST:-etcd} ETCD_PORT: ${ETCD_PORT:-2379} - x-redis-client-env: &redis-client-env REDIS_URL: ${REDIS_URL:-redis://redis:6379} @@ -29,7 +33,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 @@ -39,7 +43,7 @@ services: - ${PWD}/shard.yml:/app/shard.yml - ${PWD}/coverage:/app/coverage depends_on: - - auth + - build - core - elastic - etcd @@ -53,11 +57,19 @@ services: GITHUB_ACTION: ${GITHUB_ACTION:-} <<: *deployment-env # Service Hosts + <<: *build-client-env <<: *elastic-client-env <<: *etcd-client-env <<: *redis-client-env <<: *rethinkdb-client-env + build: + image: placeos/build:${PLACE_BUILD_TAG:-nightly} + restart: always + hostname: build + environment: + <<: *deployment-env + elastic: image: blacktop/elasticsearch:${ELASTIC_VERSION:-7.9.1} restart: always @@ -105,22 +117,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:${PLACE_CORE_TAG:-nightly} + image: ghcr.io/placeos/core:feat-build restart: always hostname: core depends_on: diff --git a/shard.lock b/shard.lock index 82be725b..d4a02dae 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.6 active-model: git: https://github.com/spider-gazelle/active-model.git @@ -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 + + 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.10.0 + 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/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 @@ -137,6 +153,14 @@ shards: git: https://github.com/luckyframework/lucky_router.git version: 0.5.1 + lz4: + git: https://github.com/naqvis/lz4.cr.git + version: 0.1.4 + + 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 @@ -159,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 @@ -167,11 +191,11 @@ 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: + opentelemetry-sdk: # Overridden git: https://github.com/wyhaines/opentelemetry-sdk.cr.git - version: 0.5.0+git.commit.5629aa05bfc0837abe6efd8f19ad28b560d9b6b7 + version: 0.5.1+git.commit.857f4dc24e3c4f2ed04b81118b39beccdecdb105 pars: # Overridden git: https://github.com/spider-gazelle/pars.git @@ -185,21 +209,25 @@ shards: git: https://github.com/spider-gazelle/pinger.git version: 1.1.2 + placeos-build: + git: https://github.com/placeos/build.git + version: 1.0.6+git.commit.f8d65b6ed947b1c28378214a9df54261bff2bd76 + placeos-compiler: git: https://github.com/placeos/compiler.git version: 4.9.2 placeos-core: git: https://github.com/placeos/core.git - version: 4.3.1+git.commit.bd4e5ff5194118b66e1f25d12b192530c9766e5f + version: 4.3.1+git.commit.29ed1ca9807ab75a597cbe42ad898163b2ee7005 - placeos-core-client: + placeos-core-client: # Overridden git: https://github.com/placeos/core-client.git - version: 0.5.2 + version: 1.0.0 placeos-driver: git: https://github.com/placeos/driver.git - version: 6.3.11 + version: 6.4.4 placeos-frontend-loader: git: https://github.com/placeos/frontend-loader.git @@ -207,11 +235,11 @@ shards: placeos-log-backend: git: https://github.com/place-labs/log-backend.git - version: 0.11.1 + version: 0.11.2 placeos-models: git: https://github.com/placeos/models.git - version: 8.11.0 + version: 8.11.2 placeos-resource: git: https://github.com/place-labs/resource.git @@ -267,12 +295,16 @@ shards: search-ingest: git: https://github.com/placeos/search-ingest.git - version: 2.3.3 + version: 2.3.4 secrets-env: # Overridden git: https://github.com/spider-gazelle/secrets-env.git version: 1.3.1 + shards: + git: https://github.com/crystal-lang/shards.git + version: 0.17.0 + simple_retry: git: https://github.com/spider-gazelle/simple_retry.git version: 1.1.1 diff --git a/shard.override.yml b/shard.override.yml index 993d6bf0..3249cf93 100644 --- a/shard.override.yml +++ b/shard.override.yml @@ -1,5 +1,18 @@ dependencies: pars: github: spider-gazelle/pars + secrets-env: github: spider-gazelle/secrets-env + + placeos-core-client: + github: placeos/core-client + version: ~> 1.0 + + action-controller: + github: spider-gazelle/action-controller + version: ~> 5.1 + + opentelemetry-sdk: + github: wyhaines/opentelemetry-sdk.cr + branch: main diff --git a/shard.yml b/shard.yml index 663f39b3..ffdeddf6 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: @@ -41,10 +41,15 @@ dependencies: github: spider-gazelle/pinger version: ~> 1 + # For build client + placeos-build: + github: placeos/build + branch: main + # 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..16d76f8b 100644 --- a/spec/controllers/api_key_spec.cr +++ b/spec/controllers/api_key_spec.cr @@ -1,25 +1,22 @@ require "../helper" -require "../scope_helper" module PlaceOS::Api describe ApiKeys do - _, scoped_authorization_header = x_api_authentication - base = ApiKeys::NAMESPACE[0] + _, scoped_headers = Spec::Authentication.x_api_authentication + before_all { _, scoped_headers = Spec::Authentication.x_api_authentication } - with_server do - Specs.test_404(base, model_name: Model::ApiKey.table_name, headers: scoped_authorization_header) + Spec.test_404(ApiKeys.base_route, model_name: Model::ApiKey.table_name, headers: Spec::Authentication.headers) - describe "index", tags: "search" do - Specs.test_base_index(Model::ApiKey, ApiKeys) - end + describe "index", tags: "search" do + Spec.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 + Spec.test_crd(Model::ApiKey, ApiKeys) + end - describe "scopes" do - Specs.test_controller_scope(ApiKeys) - end + describe "scopes" do + 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 ea2020e9..01c58540 100644 --- a/spec/controllers/asset_instances_spec.cr +++ b/spec/controllers/asset_instances_spec.cr @@ -3,84 +3,76 @@ 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, - ) - - describe "index", tags: "search" do - Specs.test_base_index(klass: Model::AssetInstance, controller_klass: AssetInstances) + Spec.test_404( + AssetInstances.base_route, + model_name: Model::AssetInstance.table_name, + headers: Spec::Authentication.headers, + ) + + describe "index", tags: "search" do + Spec.test_base_index(klass: Model::AssetInstance, controller_klass: PlaceOS::Api::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: Spec::Authentication.headers, + ) + + 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: Spec::Authentication.headers, + ) + + 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: Spec::Authentication.headers, + ) + + 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: Spec::Authentication.headers) + 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..db815914 100644 --- a/spec/controllers/asset_spec.cr +++ b/spec/controllers/asset_spec.cr @@ -2,85 +2,77 @@ require "../helper" module PlaceOS::Api describe Assets do - _, authorization_header = authentication - base = Assets::NAMESPACE[0] + Spec.test_404(Assets.base_route, model_name: Model::Asset.table_name, headers: Spec::Authentication.headers) - with_server do - Specs.test_404(base, model_name: Model::Asset.table_name, headers: authorization_header) + describe "index", tags: "search" do + Spec.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! } - 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 + response = client.get( + path: File.join(Assets.base_route, asset.id.not_nil!, "asset_instances"), + headers: Spec::Authentication.headers, + ) - 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 + Spec.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: Spec::Authentication.headers, + ) - 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 = File.join(Assets.base_route, "#{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: Spec::Authentication.headers, + ) + + 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 + 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 3f141f0d..515d5fae 100644 --- a/spec/controllers/brokers_spec.cr +++ b/spec/controllers/brokers_spec.cr @@ -2,45 +2,39 @@ require "../helper" module PlaceOS::Api describe Brokers do - _, authorization_header = authentication - base = Brokers::NAMESPACE[0] + Spec.test_404(Brokers.base_route, model_name: Model::Broker.table_name, headers: Spec::Authentication.headers) - with_server do - Specs.test_404(base, model_name: Model::Broker.table_name, headers: authorization_header) + describe "index", tags: "search" do + Spec.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 + Spec.test_crd(Model::Broker, Brokers) - 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 + it "update" do + broker = Model::Generator.broker.save! + original_name = broker.name + broker.name = random_name + + id = broker.id.as(String) + path = File.join(Brokers.base_route, id) + result = client.patch( + path: path, + body: broker.changed_attributes.to_json, + headers: Spec::Authentication.headers, + ) - describe "scopes" do - Specs.test_update_write_scope(Brokers) - Specs.test_controller_scope(Brokers) + 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 + 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 fd925b81..0644d2e6 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 + 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) + 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, 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 } + !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) + 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 + 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 + Spec.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: Spec::Authentication.headers, ) + 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: Spec::Authentication.headers, ) - 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 + it "GET /:id/compiled" do + driver = get_driver + + Utils::Changefeeds.await_model_change(driver, timeout: 20.seconds) do |update| + update.destroyed? || !update.recompile_commit? end - Specs.test_controller_scope(Drivers) + path = File.join(Drivers.base_route, "#{driver.id.not_nil!}/compiled") + response = client.get( + path: path, + headers: Spec::Authentication.headers, + ) + + response.success?.should be_true + end + + it "POST /:id/recompile" do + driver = get_driver + path = File.join(Drivers.base_route, "#{driver.id.not_nil!}/recompile") + response = client.post( + path: path, + headers: Spec::Authentication.headers, + ) + + 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 + + Spec.test_controller_scope(Drivers) + end end end diff --git a/spec/controllers/edges_spec.cr b/spec/controllers/edges_spec.cr index cec66e63..1b7eab26 100644 --- a/spec/controllers/edges_spec.cr +++ b/spec/controllers/edges_spec.cr @@ -3,70 +3,65 @@ require "placeos-core/placeos-edge/client" module PlaceOS::Api describe Edges do - authenticated_user, authorization_header = authentication - base = Edges::NAMESPACE[0] + Spec.test_404(Edges.base_route, model_name: Model::Edge.table_name, headers: Spec::Authentication.headers) - with_server do - Specs.test_404(base, model_name: Model::Edge.table_name, headers: authorization_header) + describe "index", tags: "search" do + Spec.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_host = "localhost" - 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 + new_edge = Model::Edge.for_user( + user: Spec::Authentication.user, + name: random_name, + ) - 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! + path = File.join(Edges.base_route, "control") - 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, 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 - end + 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 end end + end - describe "CRUD operations", tags: "crud" do - Specs.test_crd(Model::Edge, Edges) + describe "CRUD operations", tags: "crud" do + Spec.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: Spec::Authentication.headers, + ) - 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 + Spec.test_controller_scope(Edges) end end end diff --git a/spec/controllers/metadata_spec.cr b/spec/controllers/metadata_spec.cr index 7476adcb..9a5ee83a 100644 --- a/spec/controllers/metadata_spec.cr +++ b/spec/controllers/metadata_spec.cr @@ -3,12 +3,238 @@ require "timecop" module PlaceOS::Api describe Metadata do - _authenticated_user, authorization_header = authentication - base = Metadata::NAMESPACE[0] + 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: Spec::Authentication.headers, + ) + + 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: Spec::Authentication.headers, + ) + + 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 = "#{Api::Metadata.base_route}/#{parent_id}" + + result = client.put( + path: path, + body: meta.to_json, + headers: Spec::Authentication.headers, + ) + + 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: Spec::Authentication.headers, + ) + + 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: Spec::Authentication.headers, + ) + + 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: Spec::Authentication.headers, + ) + + 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: Spec::Authentication.headers, + ) + + 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: Spec::Authentication.headers, + ) + + 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: Spec::Authentication.headers, + ) + + 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: Spec::Authentication.headers, + ) + + 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_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new(scope_name, :read)]) - with_server do - describe "GET /metadata/:id/children/" do - it "shows zone children metadata" do parent = Model::Generator.zone.save! parent_id = parent.id.as(String) @@ -19,12 +245,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_headers, ) - + 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 +259,35 @@ module PlaceOS::Api parent.destroy end - it "filters zone children metadata" do + it "should not allow access to delete" do + _, 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) - 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_headers, ) - - 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}" - - result = curl( - method: "PUT", - path: path, - body: meta.to_json, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) - - result.status_code.should eq 201 + context "write" do + scope_name = "metadata" - 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 "should allow access to update" do + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new(scope_name, :write)]) - it "updates metadata" do parent = Model::Generator.zone.save! meta = Model::Metadata::Interface.new( name: "test", @@ -103,285 +298,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_headers, ) - 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! - - result = curl( - method: "GET", - path: "#{base}/#{zone_id}?name=special", - headers: authorization_header, - ) + it "should not allow access to show" do + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new(scope_name, :write)]) - 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_headers, ) - - 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)]) + it "checks that guests can read metadata" do + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("guest", PlaceOS::Model::UserJWT::Scope::Access::Read)]) - parent = Model::Generator.zone.save! - parent_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! - 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}/#{zone_id}", + headers: scoped_headers, + ) - id = parent.id.as(String) - - 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 = 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_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) + 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_headers, + ) + result.success?.should be_false end end end diff --git a/spec/controllers/modules_spec.cr b/spec/controllers/modules_spec.cr index 494a7cac..a6ce10f9 100644 --- a/spec/controllers/modules_spec.cr +++ b/spec/controllers/modules_spec.cr @@ -1,192 +1,168 @@ 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 - _authenticated_user, authorization_header = authentication - base = Modules::NAMESPACE[0] - - with_server do - Specs.test_404(base, 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) + describe "CRUD operations", tags: "crud" do + 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! - 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: Spec::Authentication.headers, + ) - 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: Spec::Authentication.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 - 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 = 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 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)}}", + headers: Spec::Authentication.headers, + ) - controller = Api::Modules.new(ctx, :index) + 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 - # 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, 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) + !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, Spec::Authentication.headers) 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, Spec::Authentication.headers) 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 @@ -219,11 +195,10 @@ module PlaceOS::Api driver.settings, ].flat_map(&.compact_map(&.id)).reverse! - path = "#{base}#{mod.id}/settings" - result = curl( - method: "GET", + path = File.join(Modules.base_route, "#{mod.id}/settings") + result = client.get( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) result.success?.should be_true @@ -246,12 +221,11 @@ module PlaceOS::Api control_system.update! mod = Model::Generator.module(driver: driver, control_system: control_system).save! - path = "#{base}#{mod.id}/settings" + path = File.join(Modules.base_route, "#{mod.id}/settings") - result = curl( - method: "GET", + result = client.get( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) unless result.success? @@ -265,12 +239,11 @@ 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 = File.join(Modules.base_route, "#{mod.id}/settings") - result = curl( - method: "GET", + result = client.get( path: path, - headers: authorization_header, + headers: Spec::Authentication.headers, ) unless result.success? @@ -280,69 +253,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 = File.join(Modules.base_route, "#{mod.id}/ping") + result = client.post( + path: path, + headers: Spec::Authentication.headers, + ) - 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 = File.join(Modules.base_route, "#{mod.id}/ping") + result = client.post( + path: path, + headers: Spec::Authentication.headers, + ) - 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 + 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)]) - driver = Model::Generator.driver(role: Model::Driver::Role::Service).save! - mod = Model::Generator.module(driver: driver).save! + it "checks scope on update" do + _, 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! - 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_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 + 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_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 + 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..cab2a7d6 100644 --- a/spec/controllers/mqtt_spec.cr +++ b/spec/controllers/mqtt_spec.cr @@ -1,47 +1,45 @@ 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_headers = Spec::Authentication.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 do + 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_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) - 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_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) - 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, _ = 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 end end diff --git a/spec/controllers/repositories_spec.cr b/spec/controllers/repositories_spec.cr index 9cc0c353..06b07263 100644 --- a/spec/controllers/repositories_spec.cr +++ b/spec/controllers/repositories_spec.cr @@ -2,107 +2,139 @@ require "../helper" module PlaceOS::Api describe Repositories do - _authenticated_user, authorization_header = authentication - base = Repositories::NAMESPACE[0] + Spec.test_404(Repositories.base_route, model_name: Model::Repository.table_name, headers: Spec::Authentication.headers) - with_server do - Specs.test_404(base, model_name: Model::Repository.table_name, headers: authorization_header) + describe "index", tags: "search" do + Spec.test_base_index(Model::Repository, Repositories) + end + + describe "CRUD operations", tags: "crud" do + Spec.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: Spec::Authentication.headers, + ) + + result.status_code.should eq 200 + updated = Model::Repository.from_trusted_json(result.body) - describe "index", tags: "search" do - Specs.test_base_index(Model::Repository, Repositories) + 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: Spec::Authentication.headers, + ) + + 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: Spec::Authentication.headers, ) result.status_code.should eq 200 - updated = Model::Repository.from_trusted_json(result.body) + end + end - updated.id.should eq repository.id - updated.name.should_not eq original_name + describe "driver only actions" do + repo = Model::Generator.repository(type: :interface) + before_all do + repo.save! 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 if enumerating drivers in an interface repo" do + id = repo.id.as(String) + path = File.join(Repositories.base_route, "#{id}/drivers") + result = client.get( + path: path, + headers: Spec::Authentication.headers, + ) + + result.status.should eq HTTP::Status::BAD_REQUEST 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! - - id = repository.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 - repository = Model::Generator.repository(type: Model::Repository::Type::Interface).save! - - id = repository.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 + it "errors when requesting driver details from an interface repo" do + id = repo.id.as(String) + path = File.join(Repositories.base_route, "#{id}/details") + result = client.get( + path: path, + headers: Spec::Authentication.headers, + ) + + result.status.should eq HTTP::Status::BAD_REQUEST 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) + + 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") + 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 - describe "scopes" do - Specs.test_controller_scope(Repositories) - Specs.test_update_write_scope(Repositories) + it "fetches commits for a file" do + id = repo.id.as(String) + 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 end end + + describe "scopes" do + 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 9f966473..64277fc4 100644 --- a/spec/controllers/root_spec.cr +++ b/spec/controllers/root_spec.cr @@ -2,62 +2,96 @@ require "../helper" module PlaceOS::Api describe Root do - with_server do - _authenticated_user, authorization_header = authentication - base = Api::Root::NAMESPACE[0] + describe "GET /" do + it "responds to health checks" do + result = client.get(Root.base_route, headers: Spec::Authentication.headers) + result.status_code.should eq 200 + end + end - 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 /scopes" do + it "gets scope names" do + 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 + 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 /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 + end - describe "GET /cluster/versions" do - it "constructs service versions" do - HttpMocks.service_version + describe "GET /version" do + it "renders version" do + 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) - versions = Root.construct_versions - versions.size.should eq(Root::SERVICES.size) - versions.map(&.service.gsub('-', '_')).sort!.should eq Root::SERVICES.sort - 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 /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 /platform" do + it "renders platform information" do + 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) - 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.version.should eq PLATFORM_VERSION + response.changelog.should eq PLATFORM_CHANGELOG 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 "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 - response.version.should eq PLATFORM_VERSION - response.changelog.should eq PLATFORM_CHANGELOG + _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: Spec::Authentication.headers) + 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 - 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: Spec::Authentication.headers) + result.status_code.should eq 400 + end + + context "guest users" do + _, 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) + 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 +100,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 +114,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..1d8ae176 100644 --- a/spec/controllers/settings_spec.cr +++ b/spec/controllers/settings_spec.cr @@ -2,228 +2,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 + Spec.test_404(Settings.base_route, model_name: Model::Settings.table_name, headers: Spec::Authentication.headers) - 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 = 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" + 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 = 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" + 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: Spec::Authentication.headers + ) - 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: Spec::Authentication.headers + ) - 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: Spec::Authentication.headers + ) + + 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: Spec::Authentication.headers + ) - 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: Spec::Authentication.headers + ) - 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 + 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 + 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: Spec::Authentication.headers, + ) + + 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 + 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)]) - 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_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(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_headers) - 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_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 - 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..20e4eabc 100644 --- a/spec/controllers/system-triggers_spec.cr +++ b/spec/controllers/system-triggers_spec.cr @@ -3,133 +3,126 @@ 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 + Spec.test_404( + SystemTriggers.base_route.gsub(/:sys_id/, "sys-#{Random.rand(9999)}"), + model_name: Model::TriggerInstance.table_name, + headers: Spec::Authentication.headers, + ) + + 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, 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) + + !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: Spec::Authentication.headers, + ) + + 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: Spec::Authentication.headers) - 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: Spec::Authentication.headers, + ) - 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: Spec::Authentication.headers) + 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..cb80b1a0 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, ) @@ -37,613 +35,593 @@ module PlaceOS::Api end 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) - - describe "index", tags: "search" do - Specs.test_base_index(klass: Model::ControlSystem, controller_klass: Systems) + Spec.test_404(Systems.base_route, model_name: Model::ControlSystem.table_name, headers: Spec::Authentication.headers) - context "query parameter" do - it "zone_id filters systems by zones" do - Model::ControlSystem.clear + describe "index", tags: "search" do + Spec.test_base_index(klass: Model::ControlSystem, controller_klass: Systems) - num_systems = 5 + context "query parameter" do + it "zone_id filters systems by zones" do + Model::ControlSystem.clear - zone = Model::Generator.zone.save! - zone_id = zone.id.as(String) + num_systems = 5 - systems = Array.new(size: num_systems) do - Model::Generator.control_system - end + zone = Model::Generator.zone.save! + zone_id = zone.id.as(String) - # 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! + 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.zones = [zone_id] + end + systems.each &.save! - params = HTTP::Params.encode({"zone_id" => zone_id}) - path = "#{base}?#{params}" + expected_ids = expected_systems.compact_map(&.id) + total_ids = expected_ids.size - 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 + params = HTTP::Params.encode({"zone_id" => zone_id}) + path = "#{Systems.base_route}?#{params}" - found.should be_true + refresh_elastic(Model::ControlSystem.table_name) + 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 - 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, 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 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, 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 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: Spec::Authentication.headers, + ) - 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, Spec::Authentication.headers) + {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: Spec::Authentication.headers, + ) - 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, 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) - end + spec_add_module(cs1, mod, Spec::Authentication.headers) 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, Spec::Authentication.headers) - 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, Spec::Authentication.headers) - 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 = File.join(Systems.base_route, "#{control_system.id}/settings") + result = client.get( + path: path, + headers: Spec::Authentication.headers, + ) - 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 = File.join(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: Spec::Authentication.headers, + ) - 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.body.includes?("function1").should be_true - end + result = client.get( + path: path, + headers: Spec::Authentication.headers, + ) - 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: Spec::Authentication.headers, + ) - 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: Spec::Authentication.headers, + ) - it "GET /systems/:sys_id/:module_slug" do - module_slug = cs.modules.first + Int32.from_json(response.body).should eq(1) + end - # Create a storage proxy - driver_proxy = PlaceOS::Driver::RedisStorage.new mod.id.as(String) + it "GET /systems/:sys_id/:module_slug" do + module_slug = cs.modules.first - status_name = "nugget" - driver_proxy[status_name] = 1 + # Create a storage proxy + driver_proxy = PlaceOS::Driver::RedisStorage.new 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) + status_name = "nugget" + driver_proxy[status_name] = 1 - path = base + "#{cs.id}/#{module_slug}" + sys_lookup = PlaceOS::Driver::RedisStorage.new(cs.id.as(String), "system") + lookup_key = "#{module_slug}/1" + sys_lookup[lookup_key] = mod.id.as(String) - response = curl( - method: "GET", - path: path, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) + path = Systems.base_route + "#{cs.id}/#{module_slug}" - state = Hash(String, String).from_json(response.body) - state["nugget"].should eq("1") - end + response = client.get( + path: path, + headers: Spec::Authentication.headers, + ) + + 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: Spec::Authentication.headers, + ) - 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: Spec::Authentication.headers, + ) - 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: Spec::Authentication.headers, + ) - 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 + Spec.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: Spec::Authentication.headers, + ) - 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: Spec::Authentication.headers, + ) - 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: Spec::Authentication.headers, + ) - 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 + Spec.test_controller_scope(Systems) + it "should not allow start" do + _, 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! - 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_headers, + ) - 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_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! - 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_headers, + ) - 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..8a975c0e 100644 --- a/spec/controllers/triggers_spec.cr +++ b/spec/controllers/triggers_spec.cr @@ -2,85 +2,78 @@ require "../helper" module PlaceOS::Api describe Triggers do - _authenticated_user, authorization_header = authentication - base = Triggers::NAMESPACE[0] + Spec.test_404(Triggers.base_route, model_name: Model::Trigger.table_name, headers: Spec::Authentication.headers) - with_server do - Specs.test_404(base, model_name: Model::Trigger.table_name, headers: authorization_header) + describe "index", tags: "search" do + Spec.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: Spec::Authentication.headers, + ) - # 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 + Spec.test_crd(klass: Model::Trigger, controller_klass: Triggers) - trigger.name = random_name + it "update" do + trigger = Model::Generator.trigger.save! + original_name = trigger.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"}), - ) + trigger.name = random_name - result.status_code.should eq 200 - updated = Model::Trigger.from_trusted_json(result.body) + id = trigger.id.as(String) + path = File.join(Triggers.base_route, id) + result = client.patch( + path: path, + body: trigger.to_json, + headers: Spec::Authentication.headers, + ) - updated.id.should eq trigger.id - updated.name.should_not eq original_name - updated.destroy - end + result.status_code.should eq 200 + updated = Model::Trigger.from_trusted_json(result.body) - 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) + updated.id.should eq trigger.id + updated.name.should_not eq original_name + updated.destroy + end - params = HTTP::Params{"instances" => "true"} - path = "#{base}#{trigger.id}?#{params}" + 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) - result = curl( - method: "GET", - path: path, - headers: authorization_header.merge({"Content-Type" => "application/json"}), - ) + params = HTTP::Params{"instances" => "true"} + path = File.join(Triggers.base_route, "#{trigger.id}?#{params}") - response = JSON.parse(result.body) - response["trigger_instances"].as_a?.try &.first?.try &.["id"].to_s.should eq trigger_instance_id - end + result = client.get( + path: path, + headers: Spec::Authentication.headers, + ) + + 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 + 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 5751f0be..dc45cb8d 100644 --- a/spec/controllers/users_spec.cr +++ b/spec/controllers/users_spec.cr @@ -2,179 +2,165 @@ require "../helper" module PlaceOS::Api describe Users do - authenticated_user, authorization_header = authentication - base = Users::NAMESPACE[0] + Spec.test_404(Users.base_route, model_name: Model::User.table_name, headers: Spec::Authentication.headers) - 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: Spec::Authentication.headers, + ) - 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: Spec::Authentication.headers, + ) - 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: Spec::Authentication.headers, + ) + + 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: Spec::Authentication.headers, + ) + + 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: Spec::Authentication.headers, + ) + + 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: Spec::Authentication.headers ) 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, - ) + describe "GET /users/current" do + it "renders the current user" do + result = client.get( + path: File.join(Users.base_route, "/current"), + 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 - end + result.status_code.should eq 200 + response_user = Model::User.from_trusted_json(result.body) + response_user.id.should eq Spec::Authentication.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: Spec::Authentication.headers, + ) - 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 + Spec.test_controller_scope(Users) end end end diff --git a/spec/controllers/zones_spec.cr b/spec/controllers/zones_spec.cr index c4063274..4a59044b 100644 --- a/spec/controllers/zones_spec.cr +++ b/spec/controllers/zones_spec.cr @@ -2,67 +2,60 @@ require "../helper" module PlaceOS::Api describe Zones do - _authenticated_user, authorization_header = authentication - base = Zones::NAMESPACE[0] + Spec.test_404(Zones.base_route, model_name: Model::Zone.table_name, headers: Spec::Authentication.headers) - with_server do - Specs.test_404(base, 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 + Spec.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 + Spec.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: Spec::Authentication.headers, + ) + + 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: Spec::Authentication.headers, + ) - 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 + 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 9d0add6f..076b335e 100644 --- a/spec/helper.cr +++ b/spec/helper.cr @@ -1,34 +1,49 @@ +require "action-controller/spec_helper" require "http" require "mutex" require "promise" require "random" require "rethinkdb-orm" require "simple_retry" +require "spec" + +require "./spec_helpers/*" -# Helper methods for testing controllers (curl, with_server, context) -require "../lib/action-controller/spec/curl_context" +include PlaceOS::Api::SpecClient -require "./spec_constants" -require "./scope_helper" -require "./http_mocks" +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 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" # Generators for Engine models require "placeos-models/spec/generator" -require "spec" # Configure DB db_name = "place_#{ENV["SG_ENV"]? || "development"}" @@ -39,8 +54,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, @@ -48,77 +65,23 @@ 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 = { - "X-API-Key" => api_key.x_api_key.not_nil!, - } - - {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 - 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 - authorization_header = { - "Authorization" => "Bearer #{PlaceOS::Model::Generator.jwt(authenticated_user, scope).encode}", - } - {authenticated_user, authorization_header} - 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| - user.email = test_user_email - user.sys_admin = sys_admin - user.support = support - user.save! - 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}" @@ -144,9 +107,7 @@ def until_expected(method, path, headers, timeout : Time::Span = 3.seconds, &blo rescue end - success = channel.receive? - channel.close - !!success + !!channel.receive?.tap { channel.close } end def random_name @@ -158,252 +119,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 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/authentication.cr b/spec/spec_helpers/authentication.cr new file mode 100644 index 00000000..91f44c28 --- /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) + + def self.authenticated : Tuple(Model::User, HTTP::Headers) + authentication + end + + def self.user : Model::User + CREATION_LOCK.synchronize do + authenticated.first + end + end + + def self.headers : HTTP::Headers + 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/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..2a88f15a --- /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_headers) + client.get( + path: File.join(base, id), + headers: scoped_headers, + ) + end + + def index(path, scoped_headers) + client.get( + path: path, + headers: scoped_headers, + ) + end + + def create(path, body, scoped_headers) + client.post( + path: path, + body: body, + headers: scoped_headers, + ) + end + + def delete(base, id, scoped_headers) + client.delete( + path: File.join(base, id), + headers: scoped_headers, + ) + end + + def update(path, body, scoped_headers) + client.patch( + path: path, + body: body.to_json, + headers: scoped_headers, + ) + end +end diff --git a/spec/spec_helpers/spec.cr b/spec/spec_helpers/spec.cr new file mode 100644 index 00000000..2019daf1 --- /dev/null +++ b/spec/spec_helpers/spec.cr @@ -0,0 +1,240 @@ +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 + 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 + _, headers = Spec::Authentication.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}}.base_route.rstrip('/')}?#{params}" + + found = until_expected("GET", path, headers) 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 + body = PlaceOS::Model::Generator.{{ klass_name.id }}.to_json + result = client.post( + {{ controller_klass }}.base_route, + body: body, + headers: Spec::Authentication.headers + ) + + 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 + 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: Spec::Authentication.headers, + ) + + 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 + 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: Spec::Authentication.headers + ) + + result.status_code.should eq 200 + {{ klass.id }}.find(id).should be_nil + end + end + + macro test_crd(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) + {% 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_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_headers) + 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_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :read)]) + + result = Scopes.index({{ base }}, scoped_headers) + result.success?.should be_true + end + + it "should not allow access to create" do + _, 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_headers) + result.status_code.should eq 403 + end + + it "should not allow access to delete" do + _, 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_headers) + 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_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_headers) + result.status_code.should eq 403 + model.destroy + end + + it "should not allow access to index" do + _, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new({{scope_name}}, :write)]) + + result = Scopes.index({{ base }}, scoped_headers) + result.status_code.should eq 403 + end + + it "should allow access to create" do + _, 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_headers) + 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_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_headers) + 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_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_headers) + + 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_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 + end + end +end diff --git a/spec/websocket/session_spec.cr b/spec/websocket/session_spec.cr index 43b4ee82..91fc982e 100644 --- a/spec/websocket/session_spec.cr +++ b/spec/websocket/session_spec.cr @@ -1,211 +1,193 @@ -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 + describe "systems/control" do + it "opens a websocket session" do + bind(Systems.base_route, Spec::Authentication.headers) 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 - 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, Spec::Authentication.headers) 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, Spec::Authentication.headers) 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, Spec::Authentication.headers) 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, Spec::Authentication.headers) 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, Spec::Authentication.headers) 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 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 } @@ -218,7 +200,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! @@ -235,7 +217,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 @@ -246,7 +228,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! @@ -272,7 +254,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 59b55d44..a4ca9395 100644 --- a/src/constants.cr +++ b/src/constants.cr @@ -9,24 +9,32 @@ 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_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 + + 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 - PLACE_SOURCE_HOST = ENV["PLACE_SOURCE_HOST"]? || "127.0.0.1" - PLACE_SOURCE_PORT = (ENV["PLACE_SOURCE_PORT"]? || 3000).to_i + 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") INFLUX_API_KEY = ENV["INFLUX_API_KEY"]? INFLUX_HOST = ENV["INFLUX_HOST"]? INFLUX_ORG = ENV["INFLUX_ORG"]? || "placeos" - # server defaults in `./app.cr` - TRIGGERS_URI = URI.parse(ENV["TRIGGERS_URI"]? || "http://triggers:3000") - - PROD = ENV["SG_ENV"]?.try(&.downcase) == "production" - # CHANGELOG ################################################################################################# @@ -37,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/src/placeos-rest-api/controllers/application.cr b/src/placeos-rest-api/controllers/application.cr index 728b30bf..379468e8 100644 --- a/src/placeos-rest-api/controllers/application.cr +++ b/src/placeos-rest-api/controllers/application.cr @@ -122,6 +122,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 192d3f3a..037ae3eb 100644 --- a/src/placeos-rest-api/controllers/drivers.cr +++ b/src/placeos-rest-api/controllers/drivers.cr @@ -57,7 +57,7 @@ module PlaceOS::Api include_compilation_status = boolean_param("compilation_status", default: true) 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 @@ -110,15 +110,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" } @@ -136,44 +131,20 @@ 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_path(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 end end diff --git a/src/placeos-rest-api/controllers/modules.cr b/src/placeos-rest-api/controllers/modules.cr index fb37ea55..90cf5173 100644 --- a/src/placeos-rest-api/controllers/modules.cr +++ b/src/placeos-rest-api/controllers/modules.cr @@ -306,12 +306,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) diff --git a/src/placeos-rest-api/controllers/repositories.cr b/src/placeos-rest-api/controllers/repositories.cr index 791c7025..27454d2e 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-frontend-loader/client" require "./application" @@ -18,16 +19,23 @@ module PlaceOS::Api # Callbacks ############################################################################################### - 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] 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 + getter current_repository : Model::Repository do + id = params["id"] + Log.context.set(repository_id: id) + # Find will raise a 404 (not found) if there is an error + Model::Repository.find!(id, runopts: {"read_mode" => "majority"}) + end + # Params ############################################################################################### @@ -45,15 +53,6 @@ module PlaceOS::Api ############################################################################################### - getter current_repo : Model::Repository do - id = params["id"] - Log.context.set(repository_id: id) - # Find will raise a 404 (not found) if there is an error - Model::Repository.find!(id, runopts: {"read_mode" => "majority"}) - end - - ############################################################################################### - def index elastic = Model::Repository.elastic query = elastic.query(params) @@ -62,18 +61,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 put_redirect @@ -83,12 +82,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 @@ -128,21 +127,20 @@ module PlaceOS::Api end get "/:id/drivers", :drivers do - repository_folder = current_repo.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, + ref: current_repository.commit_hash || current_repository.branch, + username: current_repository.username, + password: current_repository.decrypt_password, + request_id: request_id, + ) render json: drivers end get "/:id/commits", :commits do render json: Api::Repositories.commits( - repository: current_repo, + repository: current_repository, request_id: request_id, number_of_commits: limit, file_name: driver, @@ -153,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 @@ -168,21 +178,23 @@ module PlaceOS::Api get "/:id/details", :details do driver_filename = required_param(driver) - # 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_filename, request_id) do |core_client| - core_client.driver_details(driver_filename, commit, current_repo.folder_name) + info = Build::Client.client do |client| + client.metadata( + file: driver_filename, + 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, ) @@ -197,9 +209,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 { { @@ -214,9 +224,9 @@ module PlaceOS::Api end get "/:id/releases", :releases do - render_error(HTTP::Status::BAD_REQUEST, "can only get releases for interface repositories") unless current_repo.repo_type.interface? + render_error(HTTP::Status::BAD_REQUEST, "can only get releases for interface repositories") unless current_repository.repo_type.interface? releases = Api::Repositories.releases( - repository: current_repo, + repository: current_repository, request_id: request_id, ) diff --git a/src/placeos-rest-api/controllers/root.cr b/src/placeos-rest-api/controllers/root.cr index 11f8eaa3..5c6cf2fd 100644 --- a/src/placeos-rest-api/controllers/root.cr +++ b/src/placeos-rest-api/controllers/root.cr @@ -157,7 +157,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/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, { 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, }) diff --git a/src/placeos-rest-api/controllers/webhook.cr b/src/placeos-rest-api/controllers/webhook.cr index f015ee2a..d562dc30 100644 --- a/src/placeos-rest-api/controllers/webhook.cr +++ b/src/placeos-rest-api/controllers/webhook.cr @@ -29,7 +29,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, diff --git a/test b/test index 0ce6c536..ef5e5aec 100755 --- a/test +++ b/test @@ -13,9 +13,7 @@ function trap_ctrlc () # when signal 2 (SIGINT) is received trap "trap_ctrlc" 2 -docker-compose pull -q - -docker-compose build -q +# docker-compose pull -q exit_code="0"