diff --git a/scripts/lint.sh b/scripts/lint.sh index e1602f86117d495a0a52a76538ad37774263bf32..0de7945ed3847736b2a96c98d39f84846abed46a 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -14,4 +14,4 @@ curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bin/golangci-lint run -E gofmt -E unconvert -E misspell -E whitespace --timeout 2m --max-same-issues 10 npm install eslint@5.16.0 prettier eslint-plugin-prettier eslint-config-cozy-app -./node_modules/.bin/eslint "assets/scripts/**" +./node_modules/.bin/eslint "assets/scripts/**" tests/integration/konnector/*.js diff --git a/tests/integration/Gemfile b/tests/integration/Gemfile index 3bbffdf95eb4d8b721279eb6cf6e6b0c23ff4cb5..578998ea2ac1241b698d81ffa84da454bcc159a7 100644 --- a/tests/integration/Gemfile +++ b/tests/integration/Gemfile @@ -10,3 +10,4 @@ gem "pry" gem "pry-rescue" gem "pry-stack_explorer" gem "rest-client" +gem "uuid" diff --git a/tests/integration/Gemfile.lock b/tests/integration/Gemfile.lock index 9c0d200e150ddd57ff7a7bbde2484b667b7db7a7..f467a9fabb414999dd8c5a1311cdf1c7e2bf649e 100644 --- a/tests/integration/Gemfile.lock +++ b/tests/integration/Gemfile.lock @@ -20,6 +20,8 @@ GEM i18n (1.0.0) concurrent-ruby (~> 1.0) interception (0.5) + macaddr (1.7.2) + systemu (~> 2.6.5) method_source (0.9.0) mime-types (3.1) mime-types-data (~> 3.2015) @@ -41,9 +43,12 @@ GEM http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) + systemu (2.6.5) unf (0.1.4) unf_ext unf_ext (0.0.7.5) + uuid (2.3.9) + macaddr (~> 1.0) websocket-driver (0.7.0) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.3) @@ -62,6 +67,7 @@ DEPENDENCIES pry-rescue pry-stack_explorer rest-client + uuid BUNDLED WITH 1.16.4 diff --git a/tests/integration/boot.rb b/tests/integration/boot.rb index 93b30f7b68c75f6866d78d66421affcba9eacc02..ff795cd7f4a745097b5a7af85274b2c7b7f83ed1 100644 --- a/tests/integration/boot.rb +++ b/tests/integration/boot.rb @@ -10,6 +10,7 @@ require 'open3' require 'pbkdf2' require 'pry' require 'rest-client' +require 'uuid' AwesomePrint.pry! Pry.config.history.file = File.expand_path "../tmp/.pry_history", __FILE__ diff --git a/tests/integration/konnector/index.js b/tests/integration/konnector/index.js new file mode 100644 index 0000000000000000000000000000000000000000..48aa922c0f7e5cf08de214289bf9e89625a2168f --- /dev/null +++ b/tests/integration/konnector/index.js @@ -0,0 +1,28 @@ +let fs = require('fs') +let http = require('http') + +let fields = JSON.parse(process.env['COZY_FIELDS']) +let credentials = process.env['COZY_CREDENTIALS'] +let instance = process.env['COZY_URL'] + +let url = instance + 'data/io.cozy.accounts/' + fields.account +let options = { + headers: { + Authorization: `Bearer ${credentials}` + } +} + +http.get(url, options, res => { + if (res.statusCode !== 200) { + throw new Error(`Status Code: ${res.statusCode}`) + } + res.setEncoding('utf8') + let rawData = '' + res.on('data', chunk => { + rawData += chunk + }) + res.on('end', () => { + let data = JSON.parse(rawData) + fs.writeFileSync(data.log, JSON.stringify(data, null, ' ')) + }) +}) diff --git a/tests/integration/konnector/manifest.konnector b/tests/integration/konnector/manifest.konnector new file mode 100644 index 0000000000000000000000000000000000000000..eb61c85b28b5809bf2b171566c0c85c532aa2489 --- /dev/null +++ b/tests/integration/konnector/manifest.konnector @@ -0,0 +1,24 @@ +{ + "aggregator": { + "accountId": "bank-aggregator" + }, + "description": "A konnector to test accounts cleaning", + "developer": { + "name": "Cozy Cloud", + "url": "https://cozy.io/" + }, + "editor": "Cozy Cloud", + "language": "node", + "license": "MIT", + "name": "bankkonn", + "on_delete_account": "on_delete.js", + "permissions": { + "accounts": { + "description": "Required to read the aggregator account", + "type": "io.cozy.accounts" + } + }, + "slug": "bankkonn", + "type": "konnector", + "version": "0.1.0" +} diff --git a/tests/integration/konnector/on_delete.js b/tests/integration/konnector/on_delete.js new file mode 100644 index 0000000000000000000000000000000000000000..986d571c216c53f36f0c48eb1d7e01971fac8f50 --- /dev/null +++ b/tests/integration/konnector/on_delete.js @@ -0,0 +1,39 @@ +let fs = require('fs') +let http = require('http') + +let fields = JSON.parse(process.env['COZY_FIELDS']) +let credentials = process.env['COZY_CREDENTIALS'] +let instance = process.env['COZY_URL'] + +let url = + instance + + 'data/io.cozy.accounts/' + + fields.account + + '?rev=' + + fields.account_rev +let options = { + headers: { + Authorization: `Bearer ${credentials}` + } +} + +http.get(url, options, res => { + if (res.statusCode !== 200) { + throw new Error(`Status Code: ${res.statusCode}`) + } + res.setEncoding('utf8') + let rawData = '' + res.on('data', chunk => { + rawData += chunk + }) + res.on('end', () => { + let data = JSON.parse(rawData) + url = instance + 'data/io.cozy.accounts/' + data.relationships.data._id + http.get(url, options, res2 => { + if (res2.statusCode !== 200) { + throw new Error(`Status Code: ${res2.statusCode}`) + } + res2.pipe(fs.createWriteStream(data.log)) + }) + }) +}) diff --git a/tests/integration/lib/account.rb b/tests/integration/lib/account.rb new file mode 100644 index 0000000000000000000000000000000000000000..cfdd108fc8b15d8fe21066dcec4b4c19d0634489 --- /dev/null +++ b/tests/integration/lib/account.rb @@ -0,0 +1,34 @@ +class Account + include Model + + attr_reader :name, :log + + def self.doctype + "io.cozy.accounts" + end + + def initialize(opts = {}) + @couch_id = opts[:id] + @name = (opts[:name] || Faker::DrWho.character).gsub(/[^A-Za-z]/, '_') + @log = opts[:log] || "#{Helpers.current_dir}/account_#{@name}.log" + @aggregator = opts[:aggregator] + @type = opts[:type] + end + + def as_json + json = { + name: @name, + log: @log, + account_type: @type + }.compact + if @aggregator + json[:relationships] = { + data: { + _id: @aggregator.couch_id, + _type: @aggregator.doctype + } + } + end + json + end +end diff --git a/tests/integration/lib/album.rb b/tests/integration/lib/album.rb index 7e2ddb1e5b500c7829d8bcb4de066734e8ff2978..93ef724f3b5854649642ebb9a9f1d7a6ebcf77c0 100644 --- a/tests/integration/lib/album.rb +++ b/tests/integration/lib/album.rb @@ -56,11 +56,4 @@ class Album created_at: @created_at.rfc3339 } end - - def as_reference - { - doctype: doctype, - id: @couch_id - } - end end diff --git a/tests/integration/lib/contact.rb b/tests/integration/lib/contact.rb index 37d9068d7cc2d9c5ae7839816d75ae5ebf91dd3d..93e3b64e1a8b0efec765e834ae4e4a8809432aa1 100644 --- a/tests/integration/lib/contact.rb +++ b/tests/integration/lib/contact.rb @@ -62,11 +62,4 @@ class Contact phone: @phones } end - - def as_reference - { - doctype: doctype, - id: @couch_id - } - end end diff --git a/tests/integration/lib/instance.rb b/tests/integration/lib/instance.rb index 91185aee650cc1693a8fa9ebedd01ce32ae26ed1..815c172d493dd5516d304f8da7f2983dea142de2 100644 --- a/tests/integration/lib/instance.rb +++ b/tests/integration/lib/instance.rb @@ -25,6 +25,18 @@ class Instance @stack.install_app self, slug end + def install_konnector(slug, source_url = nil) + @stack.install_konnector self, slug, source_url + end + + def remove_konnector(slug) + @stack.remove_konnector self, slug + end + + def run_konnector(slug, account_id) + @stack.run_konnector self, slug, account_id + end + def client @client ||= RestClient::Resource.new url end diff --git a/tests/integration/lib/job.rb b/tests/integration/lib/job.rb new file mode 100644 index 0000000000000000000000000000000000000000..d80e34eb953e676186026c2e7d7a412b9589aa23 --- /dev/null +++ b/tests/integration/lib/job.rb @@ -0,0 +1,28 @@ +class Job + include Model + + attr_reader :attributes + + def self.doctype + "io.cozy.jobs" + end + + def initialize(opts = {}) + @couch_id = opts["id"] + @attributes = opts["attributes"] + end + + def done?(inst) + status(inst) == "done" + end + + def status(inst) + opts = { + accept: 'application/vnd.api+json', + authorization: "Bearer #{inst.token_for doctype}" + } + res = inst.client["/jobs/#{@couch_id}"].get opts + j = JSON.parse(res.body) + j.dig("data", "attributes", "state") + end +end diff --git a/tests/integration/lib/model.rb b/tests/integration/lib/model.rb index 076c553549c176757be6b2b6f80c5ed246a55e2c..3eaefa004825e5e3143361afc874a4f4253879e7 100644 --- a/tests/integration/lib/model.rb +++ b/tests/integration/lib/model.rb @@ -7,10 +7,17 @@ module Model end end - def to_json + def to_json(*_args) JSON.generate as_json end + def as_reference + { + doctype: doctype, + id: @couch_id + } + end + def save(inst) opts = { content_type: :json, @@ -27,6 +34,15 @@ module Model @couch_rev = j["rev"] end + def delete(inst) + opts = { + accept: "application/vnd.api+json", + content_type: "application/vnd.api+json", + authorization: "Bearer #{inst.token_for doctype}" + } + inst.client["/data/#{doctype}/#{@couch_id}?rev=#{@couch_rev}"].delete opts + end + def doctype self.class.doctype end diff --git a/tests/integration/lib/stack.rb b/tests/integration/lib/stack.rb index b2888164c2bcb4a90a7e4ee8e203cdd5c51576f7..e4f67c5d82449b3c69eae424007cc3c659fd0f0a 100644 --- a/tests/integration/lib/stack.rb +++ b/tests/integration/lib/stack.rb @@ -23,6 +23,10 @@ class Stack @tokens = {} end + def konnectors_cmd + File.expand_path "../../../scripts/konnector-node-run.sh", __dir__ + end + def start vault = File.join Helpers.current_dir, "vault" FileUtils.mkdir_p vault @@ -32,7 +36,8 @@ class Stack "--port", @port, "--admin-port", @admin, "--fs-url", "file://#{Helpers.current_dir}/", "--vault-encryptor-key", "#{vault}/key.enc", - "--vault-decryptor-key", "#{vault}/key.dec"] + "--vault-decryptor-key", "#{vault}/key.dec", + "--konnectors-cmd", konnectors_cmd] Helpers.spawn cmd.join(" "), log: "stack-#{@port}.log" sleep 1 end @@ -68,6 +73,33 @@ class Stack @apps[key] = system cmd.join(" ") end + def install_konnector(inst, slug, source_url = nil) + cmd = ["cozy-stack", "konnectors", "install", + slug, source_url, + "--port", @port, "--admin-port", @admin, + "--domain", inst.domain, ">", "/dev/null"].compact + puts cmd.join(" ").green + system cmd.join(" ") + end + + def remove_konnector(inst, slug) + cmd = ["cozy-stack", "konnectors", "uninstall", slug, + "--port", @port, "--admin-port", @admin, + "--domain", inst.domain, ">", "/dev/null"] + puts cmd.join(" ").green + system cmd.join(" ") + end + + def run_konnector(inst, slug, account_id) + cmd = ["cozy-stack", "konnectors", "run", slug, + "--account-id", account_id, + "--port", @port, "--admin-port", @admin, + "--domain", inst.domain] + puts cmd.join(" ").green + out = `#{cmd.join(" ")}`.chomp + Job.new JSON.parse(out) + end + def token_for(inst, doctypes) key = inst.domain + "/" + doctypes.join(" ") @tokens[key] ||= generate_token_for(inst, doctypes) diff --git a/tests/integration/lib/trigger.rb b/tests/integration/lib/trigger.rb new file mode 100644 index 0000000000000000000000000000000000000000..08ad8d50e6413d69aabdcb39ded2820fe361e19a --- /dev/null +++ b/tests/integration/lib/trigger.rb @@ -0,0 +1,26 @@ +class Trigger + include Model + + def self.doctype + "io.cozy.triggers" + end + + def initialize(opts = {}) + @attributes = opts + end + + def save(inst) + opts = { + content_type: :json, + accept: :json, + authorization: "Bearer #{inst.token_for doctype}" + } + res = inst.client["/jobs/triggers"].post to_json, opts + j = JSON.parse(res.body) + @couch_id = j["data"]["id"] + end + + def as_json + { data: { attributes: @attributes } } + end +end diff --git a/tests/integration/tests/accounts_cleaning.rb b/tests/integration/tests/accounts_cleaning.rb new file mode 100644 index 0000000000000000000000000000000000000000..ab0113a587638e42737898e95385f3b483ddb1e1 --- /dev/null +++ b/tests/integration/tests/accounts_cleaning.rb @@ -0,0 +1,46 @@ +require_relative '../boot' +require 'minitest/autorun' +require 'pry-rescue/minitest' unless ENV['CI'] + +def wait_for_file(file) + 10.times do + sleep 1 + return if File.exist? file + end +end + +describe "An io.cozy.accounts" do + it "is cleaned via on_delete_account" do + Helpers.scenario "accounts_cleaning" + Helpers.start_mailhog + + inst = Instance.create name: "Isabelle" + Account.create inst, name: "not a bank account" + source_url = "file://" + File.expand_path("../konnector", __dir__) + + # 1. When an account is deleted, it is cleaned. + inst.install_konnector "bankone", source_url + aggregator = Account.create inst, id: ["bank-aggregator", UUID.generate].sample + accone = Account.create inst, type: "bankone", aggregator: aggregator, + name: Faker::HarryPotter.character + Trigger.create inst, worker: "konnector", type: "@cron", arguments: "@monthly", + message: { konnector: "bankone", account: accone.couch_id } + + job = inst.run_konnector "bankone", accone.couch_id + done = false + 10.times do + sleep 1 + done = job.done?(inst) + break if done + end + assert done + executed = JSON.parse File.read(accone.log) + assert_equal executed["_id"], accone.couch_id + File.delete accone.log + + accone.delete inst + wait_for_file accone.log + executed = JSON.parse File.read(accone.log) + assert_equal executed["_id"], aggregator.couch_id + end +end