tessa 1.2.3 → 2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/README.md +77 -3
- data/docs/tessa-activestorage-sequence-diagram.drawio.png +0 -0
- data/lib/tasks/tessa.rake +12 -1
- data/lib/tessa/config.rb +1 -9
- data/lib/tessa/fake_connection.rb +29 -0
- data/lib/tessa/jobs/migrate_assets_job.rb +14 -17
- data/lib/tessa/model/dynamic_extensions.rb +5 -22
- data/lib/tessa/model.rb +4 -14
- data/lib/tessa/upload/uploads_file.rb +1 -3
- data/lib/tessa/version.rb +1 -1
- data/lib/tessa.rb +1 -1
- data/spec/support/remote_call_macro.rb +1 -6
- data/spec/tessa/config_spec.rb +0 -42
- data/spec/tessa/model_spec.rb +6 -259
- data/spec/tessa_spec.rb +23 -0
- data/tessa.gemspec +0 -1
- metadata +6 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5d98c3c1a4dd8795f5631bee2260de0c0794f4eded2c71c6cf4bfaee23dc518e
|
4
|
+
data.tar.gz: 42c2a4748df92707676e06ecbb965e5245444bf343e468a5db60ea55a6b89337
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bd8d14042de56677d144e26ff425551d5e7a68e95d331406e1556fbb34ec43f305203e139150fa05647091caaabe38c4d2df5cf29889d783665a8806a15e9ef8
|
7
|
+
data.tar.gz: d25ed80bf643b5ff4a126d67094b0ba66caf4f643f90efdfd4acec0dbbbf555bf1f60609bc51bc6bdb633cf5fb86feac3cde3d1cfb26adc4e75856cf76c55103
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,11 +1,27 @@
|
|
1
|
-
# [Tessa][0]
|
1
|
+
# [Tessa Client][0]
|
2
2
|
|
3
3
|
[ ![Gem Version][1] ][2]
|
4
4
|
[ ![Build Status][3] ][4]
|
5
5
|
[ ![Coverage Status][5] ][6]
|
6
6
|
[ ![Code Climate][7] ][8]
|
7
7
|
|
8
|
-
|
8
|
+
This gem wraps ActiveStorage to provide some convenience helpers for direct uploading
|
9
|
+
assets, as well as generating public and private asset URLs.
|
10
|
+
|
11
|
+
## History
|
12
|
+
|
13
|
+
This gem was initially conceived as a client to the
|
14
|
+
[Tessa asset management service](https://github.com/watermarkchurch/tessa). That service has been deprecated and is
|
15
|
+
being phased out.
|
16
|
+
|
17
|
+
In 2022, we moved away from using Tessa to manage our assets and got onto the official Rails ActiveStorage library.
|
18
|
+
[Here is the issue tracking the effort](https://github.com/watermarkchurch/tessa/issues/27). As part of that process,
|
19
|
+
we released the v1.x version of this gem which supports both ActiveStorage and Tessa but uploads all new assets to
|
20
|
+
ActiveStorage.
|
21
|
+
|
22
|
+
When that migration is completed, v2.x of this gem will remove all code that accesses the old Tessa service. This gem
|
23
|
+
will transition to being simply a convenience wrapper around ActiveStorage, so that we don't have to re-write as much
|
24
|
+
of our code in our frontend apps.
|
9
25
|
|
10
26
|
## Installation
|
11
27
|
|
@@ -25,7 +41,65 @@ Or install it yourself as:
|
|
25
41
|
|
26
42
|
## Usage
|
27
43
|
|
28
|
-
|
44
|
+
We provide no guarantee of support for your use of this gem. It is for internal Watermark use only.
|
45
|
+
**Use at your own risk!**
|
46
|
+
|
47
|
+
The main thing this gem still gives us is direct-uploads using [Dropzone.js](https://github.com/dropzone/dropzone).
|
48
|
+
Before using this gem it's worthwhile to understand how it works:
|
49
|
+
|
50
|
+

|
51
|
+
|
52
|
+
When an Asset input is configured using the SimpleForm helper (TODO: https://github.com/watermarkchurch/tessa-client/issues/22),
|
53
|
+
it creates a DropZone div with the css class `.tessa-upload`. The javascript
|
54
|
+
in `app/javascript/tessa/index.js.coffee` scans for these divs and configures
|
55
|
+
Dropzone.js.
|
56
|
+
|
57
|
+
When a user initiates a file upload via Dropzone, it does an XHR POST to `/tessa/uploads`. This triggers
|
58
|
+
the `lib/tessa/rack_upload_proxy.rb` middleware to create a new ActiveStorage blob, and returns the
|
59
|
+
signed upload URL. Dropzone then uploads the file directly to S3 (or wherever ActiveStorage is configured)
|
60
|
+
from the user's browser.
|
61
|
+
|
62
|
+
To get all this working, follow these steps:
|
63
|
+
|
64
|
+
1. Mount the engine
|
65
|
+
in your `config/routes.rb`, `mount Tessa::Engine, at: '/'`. It's important that it is mounted at root.
|
66
|
+
|
67
|
+
You can use Authentication around the engine to prevent unauthorized uploads. With devise it's as simple as:
|
68
|
+
```rb
|
69
|
+
authenticated :user do
|
70
|
+
mount Tessa::Engine, at: '/'
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
2. In your application.js, require the js libraries:
|
75
|
+
```js
|
76
|
+
//= require dropzone
|
77
|
+
//= require tessa
|
78
|
+
```
|
79
|
+
Note that this only works if you are using Sprockets.
|
80
|
+
If you are using another bundler, we don't support that yet.
|
81
|
+
|
82
|
+
3. In your model, use the Tessa `asset` declaration instead of `has_one_attached`. The SimpleForm helper only works
|
83
|
+
with Tessa `asset` declarations so far.
|
84
|
+
|
85
|
+
```rb
|
86
|
+
class Model < ApplicationRecord
|
87
|
+
include Tessa::Model
|
88
|
+
|
89
|
+
asset :image # Note: this essentially delegates to has_one_attached
|
90
|
+
```
|
91
|
+
|
92
|
+
4. When rendering your form, use the SimpleForm helper to render the dropzone div:
|
93
|
+
|
94
|
+
```erb
|
95
|
+
<%= f.input :image,
|
96
|
+
as: :asset,
|
97
|
+
dropzone: { acceptedFiles: "image/*" },
|
98
|
+
hint: "Use an image that is 1440 x 288 in size (5:1 aspect ratio)" %>
|
99
|
+
```
|
100
|
+
|
101
|
+
5. Configure your ActiveStorage service to accept direct uploads.
|
102
|
+
The disk service does this automatically. The S3 service requires additional CORS configuration.
|
29
103
|
|
30
104
|
## Contributing
|
31
105
|
|
Binary file
|
data/lib/tasks/tessa.rake
CHANGED
@@ -2,6 +2,17 @@
|
|
2
2
|
namespace :tessa do
|
3
3
|
desc "Begins the migration of all Tessa assets to ActiveStorage."
|
4
4
|
task :migrate => :environment do
|
5
|
-
Tessa::MigrateAssetsJob.
|
5
|
+
abort "Tessa::MigrateAssetsJob can no longer be performed because the Tessa connection was removed. "\
|
6
|
+
"Please downgrade to tessa ~>1.0 and try again."
|
7
|
+
end
|
8
|
+
|
9
|
+
desc "Verifies that the migration has completed"
|
10
|
+
task :verify => :environment do
|
11
|
+
unless Tessa::MigrateAssetsJob.complete?
|
12
|
+
state = Tessa::MigrateAssetsJob::ProcessingState.initialize_from_models
|
13
|
+
|
14
|
+
abort "Tessa::MigrateAssetsJob not yet complete! #{state.count} records remain to be migrated. "\
|
15
|
+
"Please downgrade to tessa ~>1.0 and try again."
|
16
|
+
end
|
6
17
|
end
|
7
18
|
end
|
data/lib/tessa/config.rb
CHANGED
@@ -10,15 +10,7 @@ module Tessa
|
|
10
10
|
attribute :strategy, String, default: -> (*_) { ENV['TESSA_STRATEGY'] || DEFAULT_STRATEGY }
|
11
11
|
|
12
12
|
def connection
|
13
|
-
@connection ||=
|
14
|
-
if conn.respond_to?(:basic_auth)
|
15
|
-
conn.basic_auth username, password
|
16
|
-
else # Faraday >= 1.0
|
17
|
-
conn.request :authorization, :basic, username, password
|
18
|
-
end
|
19
|
-
conn.request :url_encoded
|
20
|
-
conn.adapter Faraday.default_adapter
|
21
|
-
end
|
13
|
+
@connection ||= Tessa::FakeConnection.new
|
22
14
|
end
|
23
15
|
end
|
24
16
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Tessa
|
2
|
+
# Since we no longer connect to the Tessa service, fake out the Tessa connection
|
3
|
+
# so that it always returns 503
|
4
|
+
class FakeConnection
|
5
|
+
|
6
|
+
[:get, :head, :put, :post, :patch, :delete].each do |method|
|
7
|
+
define_method(method) do |*args|
|
8
|
+
if defined?(Bugsnag)
|
9
|
+
Bugsnag.notify("Tessa::FakeConnection##{method} invoked")
|
10
|
+
end
|
11
|
+
Tessa::FakeConnection::Response.new()
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Response
|
16
|
+
def success?
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
def status
|
21
|
+
503
|
22
|
+
end
|
23
|
+
|
24
|
+
def body
|
25
|
+
'{ "error": "The Tessa connection is no longer implemented." }'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -72,15 +72,6 @@ class Tessa::MigrateAssetsJob < ActiveJob::Base
|
|
72
72
|
reupload(record, field_state)
|
73
73
|
Rails.logger.info("#{record.class}#{record.id}##{field_state.field_name}: success")
|
74
74
|
field_state.success_count += 1
|
75
|
-
rescue OpenURI::HTTPError => ex
|
76
|
-
if ex.message == "404 Not Found"
|
77
|
-
# clear out the field if the asset is missing
|
78
|
-
record.public_send("#{field_state.tessa_field.name}=", nil)
|
79
|
-
record.save!
|
80
|
-
else
|
81
|
-
Rails.logger.error("#{record.class}#{record.id}##{field_state.field_name}: error - #{ex}")
|
82
|
-
end
|
83
|
-
field_state.offset += 1
|
84
75
|
rescue StandardError => ex
|
85
76
|
Rails.logger.error("#{record.class}#{record.id}##{field_state.field_name}: error - #{ex}")
|
86
77
|
field_state.offset += 1
|
@@ -136,18 +127,24 @@ class Tessa::MigrateAssetsJob < ActiveJob::Base
|
|
136
127
|
end
|
137
128
|
|
138
129
|
def load_models_from_registry
|
139
|
-
Rails.application.eager_load!
|
140
|
-
|
141
|
-
# Load all Tessa models that can have attachments (not form objects)
|
142
|
-
models = Tessa.model_registry
|
143
|
-
.select { |m| m.respond_to?(:has_one_attached) }
|
144
|
-
|
145
130
|
# Initialize our Record Keeping object
|
146
|
-
ProcessingState.initialize_from_models
|
131
|
+
ProcessingState.initialize_from_models
|
132
|
+
end
|
133
|
+
|
134
|
+
# Determines whether the migrate asset job is completed. If true, running the
|
135
|
+
# job again will not do anything.
|
136
|
+
def self.complete?
|
137
|
+
ProcessingState.initialize_from_models.fully_processed?
|
147
138
|
end
|
148
139
|
|
149
140
|
ProcessingState = Struct.new(:model_queue, :batch_count) do
|
150
|
-
def self.initialize_from_models(models)
|
141
|
+
def self.initialize_from_models(models = nil)
|
142
|
+
unless models
|
143
|
+
# Load all Tessa models that can have attachments (not form objects)
|
144
|
+
Rails.application.eager_load!
|
145
|
+
models = Tessa.model_registry.select { |m| m.respond_to?(:has_one_attached) }
|
146
|
+
end
|
147
|
+
|
151
148
|
new(
|
152
149
|
models.map do |model|
|
153
150
|
ModelProcessingState.initialize_from_model(model)
|
@@ -22,19 +22,11 @@ class Tessa::DynamicExtensions
|
|
22
22
|
if #{name}_attachment.present?
|
23
23
|
return Tessa::ActiveStorage::AssetWrapper.new(#{name}_attachment)
|
24
24
|
end
|
25
|
-
|
26
|
-
# fall back to old Tessa fetch if not present
|
27
|
-
if field = self.class.tessa_fields["#{name}".to_sym]
|
28
|
-
@#{name} ||= fetch_tessa_remote_assets(field.id(on: self))
|
29
|
-
end
|
30
25
|
end
|
31
26
|
|
32
27
|
def #{field.id_field}
|
33
28
|
# Use the attachment's key
|
34
29
|
return #{name}_attachment.key if #{name}_attachment.present?
|
35
|
-
|
36
|
-
# fallback to Tessa's database column
|
37
|
-
super
|
38
30
|
end
|
39
31
|
|
40
32
|
def #{name}=(attachable)
|
@@ -80,7 +72,6 @@ class Tessa::DynamicExtensions
|
|
80
72
|
|
81
73
|
@#{name} ||= [
|
82
74
|
*#{name}_attachments.map { |a| Tessa::ActiveStorage::AssetWrapper.new(a) },
|
83
|
-
*fetch_tessa_remote_assets(tessa_ids)
|
84
75
|
]
|
85
76
|
end
|
86
77
|
|
@@ -88,8 +79,6 @@ class Tessa::DynamicExtensions
|
|
88
79
|
[
|
89
80
|
# Use the attachment's key
|
90
81
|
*#{name}_attachments.map(&:key),
|
91
|
-
# include from Tessa's database column
|
92
|
-
*super
|
93
82
|
]
|
94
83
|
end
|
95
84
|
|
@@ -105,8 +94,8 @@ class Tessa::DynamicExtensions
|
|
105
94
|
existing.destroy
|
106
95
|
else
|
107
96
|
ids = self.#{field.id_field}
|
108
|
-
ids
|
109
|
-
self.#{field.id_field} = ids
|
97
|
+
ids&.delete(change.id.to_i)
|
98
|
+
self.#{field.id_field} = ids&.any? ? ids : nil
|
110
99
|
end
|
111
100
|
end
|
112
101
|
attachables.changes.select(&:add?).each do |change|
|
@@ -138,10 +127,7 @@ class Tessa::DynamicExtensions
|
|
138
127
|
def build(mod)
|
139
128
|
mod.class_eval <<~CODE, __FILE__, __LINE__ + 1
|
140
129
|
attr_accessor :#{name}_id
|
141
|
-
|
142
|
-
def #{name}
|
143
|
-
@#{name} ||= fetch_tessa_remote_assets(#{name}_id)
|
144
|
-
end
|
130
|
+
attr_accessor :#{name}
|
145
131
|
CODE
|
146
132
|
mod
|
147
133
|
end
|
@@ -151,12 +137,9 @@ class Tessa::DynamicExtensions
|
|
151
137
|
def build(mod)
|
152
138
|
mod.class_eval <<~CODE, __FILE__, __LINE__ + 1
|
153
139
|
attr_accessor :#{name}_ids
|
154
|
-
|
155
|
-
def #{name}
|
156
|
-
@#{name} ||= fetch_tessa_remote_assets(#{name}_ids)
|
157
|
-
end
|
140
|
+
attr_accessor :#{name}
|
158
141
|
CODE
|
159
142
|
mod
|
160
143
|
end
|
161
144
|
end
|
162
|
-
end
|
145
|
+
end
|
data/lib/tessa/model.rb
CHANGED
@@ -8,8 +8,6 @@ module Tessa
|
|
8
8
|
def self.included(base)
|
9
9
|
base.send :include, InstanceMethods
|
10
10
|
base.extend ClassMethods
|
11
|
-
base.after_commit :apply_tessa_change_sets if base.respond_to?(:after_commit)
|
12
|
-
base.before_destroy :remove_all_tessa_assets if base.respond_to?(:before_destroy)
|
13
11
|
|
14
12
|
Tessa.model_registry << base
|
15
13
|
end
|
@@ -21,9 +19,9 @@ module Tessa
|
|
21
19
|
end
|
22
20
|
|
23
21
|
def apply_tessa_change_sets
|
24
|
-
|
25
|
-
|
26
|
-
|
22
|
+
# Pretend like the application was successful but we didn't do anything
|
23
|
+
# because everything is in ActiveStorage now
|
24
|
+
pending_tessa_change_sets.clear
|
27
25
|
end
|
28
26
|
|
29
27
|
def remove_all_tessa_assets
|
@@ -37,17 +35,9 @@ module Tessa
|
|
37
35
|
end
|
38
36
|
|
39
37
|
def fetch_tessa_remote_assets(ids)
|
38
|
+
# This should just always return Tessa::AssetFailure
|
40
39
|
Tessa.find_assets(ids)
|
41
40
|
end
|
42
|
-
|
43
|
-
private def reapplying_asset?(field, change_set)
|
44
|
-
additions = change_set.changes.select(&:add?)
|
45
|
-
|
46
|
-
return false if additions.none?
|
47
|
-
return false if change_set.changes.size > additions.size
|
48
|
-
|
49
|
-
additions.all? { |a| field.ids(on: self).include?(a.id) }
|
50
|
-
end
|
51
41
|
end
|
52
42
|
|
53
43
|
module ClassMethods
|
data/lib/tessa/version.rb
CHANGED
data/lib/tessa.rb
CHANGED
@@ -24,16 +24,11 @@ RSpec.shared_examples_for "remote call macro" do |method, path, return_type|
|
|
24
24
|
end
|
25
25
|
|
26
26
|
context "when response is not successful" do
|
27
|
-
let(:
|
28
|
-
Faraday::Adapter::Test::Stubs.new do |stub|
|
29
|
-
stub.send(method, path) { |env| [422, {}, { "error" => "error" }.to_json] }
|
30
|
-
end
|
31
|
-
}
|
27
|
+
let(:connection) { Tessa::FakeConnection.new }
|
32
28
|
|
33
29
|
it "raises Tessa::RequestFailed" do
|
34
30
|
expect{ call }.to raise_error { |error|
|
35
31
|
expect(error).to be_a(Tessa::RequestFailed)
|
36
|
-
expect(error.response).to be_a(Faraday::Response)
|
37
32
|
}
|
38
33
|
end
|
39
34
|
end
|
data/spec/tessa/config_spec.rb
CHANGED
@@ -67,46 +67,4 @@ RSpec.describe Tessa::Config do
|
|
67
67
|
expect(cfg.strategy).to eq("my-new-value")
|
68
68
|
end
|
69
69
|
end
|
70
|
-
|
71
|
-
describe "#connection" do
|
72
|
-
it "is a Faraday::Connection" do
|
73
|
-
expect(cfg.connection).to be_a(Faraday::Connection)
|
74
|
-
end
|
75
|
-
|
76
|
-
context "with values cfgured" do
|
77
|
-
subject { cfg.connection }
|
78
|
-
before { args.each { |k, v| cfg.send("#{k}=", v) } }
|
79
|
-
let(:args) {
|
80
|
-
{
|
81
|
-
url: "http://tessa.test",
|
82
|
-
username: "username",
|
83
|
-
password: "password",
|
84
|
-
}
|
85
|
-
}
|
86
|
-
|
87
|
-
it "sets faraday's url prefix to our url" do
|
88
|
-
expect(subject.url_prefix.to_s).to match(cfg.url)
|
89
|
-
end
|
90
|
-
|
91
|
-
context "with faraday spy" do
|
92
|
-
let(:spy) { instance_spy(Faraday::Connection) }
|
93
|
-
before do
|
94
|
-
expect(Faraday).to receive(:new).and_yield(spy)
|
95
|
-
subject
|
96
|
-
end
|
97
|
-
|
98
|
-
it "sets up url_encoded request handler" do
|
99
|
-
expect(spy).to have_received(:request).with(:url_encoded)
|
100
|
-
end
|
101
|
-
|
102
|
-
it "cfgures the default adapter" do
|
103
|
-
expect(spy).to have_received(:adapter).with(:net_http)
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
it "caches the result" do
|
109
|
-
expect(cfg.connection.object_id).to eq(cfg.connection.object_id)
|
110
|
-
end
|
111
|
-
end
|
112
70
|
end
|
data/spec/tessa/model_spec.rb
CHANGED
@@ -93,17 +93,10 @@ RSpec.describe Tessa::Model do
|
|
93
93
|
let(:instance) { model.new(another_place: []) }
|
94
94
|
subject(:getter) { instance.multiple_field }
|
95
95
|
|
96
|
-
it "calls find
|
96
|
+
it "No longer calls Tessa::Asset#find" do
|
97
97
|
instance.another_place = [1, 2, 3]
|
98
|
-
expect(Tessa::Asset).
|
99
|
-
expect(getter).to eq([
|
100
|
-
end
|
101
|
-
|
102
|
-
it "caches the result" do
|
103
|
-
instance.another_place = [1]
|
104
|
-
expect(Tessa::Asset).to receive(:find).and_return(:val).once
|
105
|
-
instance.multiple_field
|
106
|
-
instance.multiple_field
|
98
|
+
expect(Tessa::Asset).to_not receive(:find)
|
99
|
+
expect(getter).to eq([])
|
107
100
|
end
|
108
101
|
|
109
102
|
context "with no values" do
|
@@ -121,17 +114,10 @@ RSpec.describe Tessa::Model do
|
|
121
114
|
}
|
122
115
|
subject(:getter) { instance.avatar }
|
123
116
|
|
124
|
-
it "calls find
|
125
|
-
instance.avatar_id = 1
|
126
|
-
expect(Tessa::Asset).to receive(:find).with(1).and_return(:a1)
|
127
|
-
expect(getter).to eq(:a1)
|
128
|
-
end
|
129
|
-
|
130
|
-
it "caches the result" do
|
117
|
+
it "No longer calls Tessa::Asset#find" do
|
131
118
|
instance.avatar_id = 1
|
132
|
-
expect(Tessa::Asset).
|
133
|
-
|
134
|
-
instance.avatar
|
119
|
+
expect(Tessa::Asset).to_not receive(:find)
|
120
|
+
expect(getter).to eq(nil)
|
135
121
|
end
|
136
122
|
|
137
123
|
it "wraps ActiveStorage uploads with AssetWrapper" do
|
@@ -334,245 +320,6 @@ RSpec.describe Tessa::Model do
|
|
334
320
|
|
335
321
|
expect(instance.another_place).to eq(keys)
|
336
322
|
end
|
337
|
-
|
338
|
-
it 'replaces Tessa assets with ActiveStorage assets' do
|
339
|
-
# Before deploying this code, we previously had DB records with Tessa IDs
|
340
|
-
instance.update!(another_place: [1, 2, 3])
|
341
|
-
|
342
|
-
# In this HTTP POST, we removed one of the tessa assets and uploaded a
|
343
|
-
# new ActiveStorage asset
|
344
|
-
blob = ::ActiveStorage::Blob.create_before_direct_upload!({
|
345
|
-
filename: 'README.md',
|
346
|
-
byte_size: file.size,
|
347
|
-
content_type: file.content_type,
|
348
|
-
checksum: '1234'
|
349
|
-
})
|
350
|
-
changeset = Tessa::AssetChangeSet.new(
|
351
|
-
changes: [
|
352
|
-
{ 'id' => 1, 'action' => 'add' },
|
353
|
-
{ 'id' => 2, 'action' => 'remove' },
|
354
|
-
{ 'id' => 3, 'action' => 'add' },
|
355
|
-
{ 'id' => blob.signed_id, 'action' => 'add' },
|
356
|
-
]
|
357
|
-
)
|
358
|
-
|
359
|
-
# We'll download these assets when we access #multiple_field
|
360
|
-
allow(Tessa.config.connection).to receive(:get)
|
361
|
-
.with("/assets/1,3")
|
362
|
-
.and_return(double("response",
|
363
|
-
success?: true,
|
364
|
-
body: [
|
365
|
-
{ 'id' => 1, 'public_url' => 'test1' },
|
366
|
-
{ 'id' => 2, 'public_url' => 'test2' }
|
367
|
-
].to_json))
|
368
|
-
|
369
|
-
blob.upload(file)
|
370
|
-
|
371
|
-
# act
|
372
|
-
instance.multiple_field = changeset
|
373
|
-
|
374
|
-
expect(instance.another_place).to eq([
|
375
|
-
blob.key, 1, 3
|
376
|
-
])
|
377
|
-
|
378
|
-
assets = instance.multiple_field
|
379
|
-
expect(assets[0].key).to eq(blob.key)
|
380
|
-
expect(assets[0].service_url)
|
381
|
-
.to start_with('https://www.example.com/rails/active_storage/disk/')
|
382
|
-
|
383
|
-
expect(assets[1].id).to eq(1)
|
384
|
-
expect(assets[1].public_url).to eq('test1')
|
385
|
-
expect(assets[2].id).to eq(2)
|
386
|
-
expect(assets[2].public_url).to eq('test2')
|
387
|
-
end
|
388
|
-
end
|
389
|
-
end
|
390
|
-
|
391
|
-
describe "#apply_tessa_change_sets" do
|
392
|
-
let(:instance) { model.new }
|
393
|
-
let(:sets) { [ instance_spy(Tessa::AssetChangeSet) ] }
|
394
|
-
|
395
|
-
before do
|
396
|
-
instance.instance_variable_set(
|
397
|
-
:@pending_tessa_change_sets,
|
398
|
-
{
|
399
|
-
avatar: sets[0],
|
400
|
-
}
|
401
|
-
)
|
402
|
-
end
|
403
|
-
|
404
|
-
it "iterates over all pending changesets calling apply" do
|
405
|
-
instance.apply_tessa_change_sets
|
406
|
-
expect(sets[0]).to have_received(:apply)
|
407
|
-
end
|
408
|
-
|
409
|
-
it "removes all changesets from list" do
|
410
|
-
instance.apply_tessa_change_sets
|
411
|
-
expect(instance.pending_tessa_change_sets).to be_empty
|
412
|
-
end
|
413
|
-
|
414
|
-
context "no @pending_tessa_change_sets ivar" do
|
415
|
-
before do
|
416
|
-
instance.instance_variable_set(
|
417
|
-
:@pending_tessa_change_sets,
|
418
|
-
nil
|
419
|
-
)
|
420
|
-
end
|
421
|
-
|
422
|
-
it "doesn't raise error" do
|
423
|
-
expect { instance.apply_tessa_change_sets }.to_not raise_error
|
424
|
-
end
|
425
323
|
end
|
426
324
|
end
|
427
|
-
|
428
|
-
describe "#fetch_tessa_remote_assets" do
|
429
|
-
subject(:result) { model.new.fetch_tessa_remote_assets(arg) }
|
430
|
-
|
431
|
-
context "argument is `nil`" do
|
432
|
-
let(:arg) { nil }
|
433
|
-
|
434
|
-
it "returns nil" do
|
435
|
-
expect(result).to be_nil
|
436
|
-
end
|
437
|
-
end
|
438
|
-
|
439
|
-
context "argument is `[]`" do
|
440
|
-
let(:arg) { [] }
|
441
|
-
|
442
|
-
it "returns []" do
|
443
|
-
expect(result).to be_a(Array)
|
444
|
-
expect(result).to be_empty
|
445
|
-
end
|
446
|
-
end
|
447
|
-
|
448
|
-
context "when argument is not blank" do
|
449
|
-
let(:id) { rand(100) }
|
450
|
-
let(:arg) { id }
|
451
|
-
|
452
|
-
it "calls Tessa::Asset.find with arguments" do
|
453
|
-
expect(Tessa::Asset).to receive(:find).with(arg)
|
454
|
-
result
|
455
|
-
end
|
456
|
-
|
457
|
-
context "when Tessa::Asset.find raises RequestFailed exception" do
|
458
|
-
let(:error) {
|
459
|
-
Tessa::RequestFailed.new("test exception", double(status: '500'))
|
460
|
-
}
|
461
|
-
|
462
|
-
before do
|
463
|
-
allow(Tessa::Asset).to receive(:find).and_raise(error)
|
464
|
-
end
|
465
|
-
|
466
|
-
context "argument is single id" do
|
467
|
-
let(:arg) { id }
|
468
|
-
|
469
|
-
it "returns Failure" do
|
470
|
-
expect(result).to be_a(Tessa::Asset::Failure)
|
471
|
-
end
|
472
|
-
|
473
|
-
it "returns asset with proper data" do
|
474
|
-
expect(result.id).to eq(arg)
|
475
|
-
end
|
476
|
-
end
|
477
|
-
|
478
|
-
context "argument is array" do
|
479
|
-
let(:arg) { [ id, id * 2 ] }
|
480
|
-
|
481
|
-
it "returns array" do
|
482
|
-
expect(result).to be_a(Array)
|
483
|
-
end
|
484
|
-
|
485
|
-
it "returns instances of Failure" do
|
486
|
-
expect(result).to all( be_a(Tessa::Asset::Failure) )
|
487
|
-
end
|
488
|
-
|
489
|
-
it "returns array with an asset for each id passed" do
|
490
|
-
arg.zip(result) do |a, r|
|
491
|
-
expect(r.id).to eq(a)
|
492
|
-
end
|
493
|
-
end
|
494
|
-
end
|
495
|
-
end
|
496
|
-
end
|
497
|
-
end
|
498
|
-
|
499
|
-
describe "#remove_all_tessa_assets" do
|
500
|
-
let(:instance) { model.new }
|
501
|
-
|
502
|
-
context "with a single typed field" do
|
503
|
-
let(:model) {
|
504
|
-
SingleAssetModel
|
505
|
-
}
|
506
|
-
|
507
|
-
before do
|
508
|
-
instance.avatar_id = 1
|
509
|
-
end
|
510
|
-
|
511
|
-
it "adds pending change sets for each field removing all current assets" do
|
512
|
-
instance.remove_all_tessa_assets
|
513
|
-
changes = instance.pending_tessa_change_sets.values
|
514
|
-
.reduce(Tessa::AssetChangeSet.new, :+)
|
515
|
-
.changes
|
516
|
-
.map { |change| [change.id, change.action.to_sym] }
|
517
|
-
expect(changes).to eq([
|
518
|
-
[1, :remove]
|
519
|
-
])
|
520
|
-
end
|
521
|
-
end
|
522
|
-
|
523
|
-
|
524
|
-
context "with a multiple typed field" do
|
525
|
-
let(:model) {
|
526
|
-
MultipleAssetModel
|
527
|
-
}
|
528
|
-
let(:instance) { model.new(another_place: []) }
|
529
|
-
|
530
|
-
before do
|
531
|
-
instance.another_place = [2, 3]
|
532
|
-
end
|
533
|
-
|
534
|
-
it "adds pending change sets for each field removing all current assets" do
|
535
|
-
instance.remove_all_tessa_assets
|
536
|
-
changes = instance.pending_tessa_change_sets.values
|
537
|
-
.reduce(Tessa::AssetChangeSet.new, :+)
|
538
|
-
.changes
|
539
|
-
.map { |change| [change.id, change.action.to_sym] }
|
540
|
-
expect(changes).to eq([
|
541
|
-
[2, :remove],
|
542
|
-
[3, :remove],
|
543
|
-
])
|
544
|
-
end
|
545
|
-
end
|
546
|
-
end
|
547
|
-
|
548
|
-
describe "adds callbacks" do
|
549
|
-
context "model responds to after_commit" do
|
550
|
-
let(:model) {
|
551
|
-
Class.new do
|
552
|
-
def self.after_commit(arg=nil)
|
553
|
-
@after_commit ||= arg
|
554
|
-
end
|
555
|
-
end.tap { |c| c.send(:include, described_module) }
|
556
|
-
}
|
557
|
-
|
558
|
-
it "calls it with :apply_tessa_change_sets" do
|
559
|
-
expect(model.after_commit).to eq(:apply_tessa_change_sets)
|
560
|
-
end
|
561
|
-
end
|
562
|
-
|
563
|
-
context "model responds to before_destroy" do
|
564
|
-
let(:model) {
|
565
|
-
Class.new do
|
566
|
-
def self.before_destroy(arg=nil)
|
567
|
-
@before_destroy ||= arg
|
568
|
-
end
|
569
|
-
end.tap { |c| c.send(:include, described_module) }
|
570
|
-
}
|
571
|
-
|
572
|
-
it "calls it with :remove_all_tessa_assets" do
|
573
|
-
expect(model.before_destroy).to eq(:remove_all_tessa_assets)
|
574
|
-
end
|
575
|
-
end
|
576
|
-
end
|
577
|
-
|
578
325
|
end
|
data/spec/tessa_spec.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
RSpec.describe Tessa do
|
4
|
+
|
5
|
+
describe '#find_assets' do
|
6
|
+
it 'returns AssetFailure for singular asset' do
|
7
|
+
result = Tessa.find_assets(1)
|
8
|
+
|
9
|
+
expect(result).to be_a(Tessa::Asset::Failure)
|
10
|
+
expect(result.message).to eq("The service is unavailable at this time.")
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'returns AssetFailure array for multiple assets' do
|
14
|
+
result = Tessa.find_assets([1, 2, 3])
|
15
|
+
|
16
|
+
expect(result.count).to eq(3)
|
17
|
+
result.each do |r|
|
18
|
+
expect(r).to be_a(Tessa::Asset::Failure)
|
19
|
+
expect(r.message).to eq("The service is unavailable at this time.")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/tessa.gemspec
CHANGED
@@ -23,7 +23,6 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
24
24
|
spec.require_paths = ["lib"]
|
25
25
|
|
26
|
-
spec.add_dependency "faraday"
|
27
26
|
spec.add_dependency "virtus", "~>1.0.4"
|
28
27
|
|
29
28
|
spec.add_development_dependency "rake", "~> 10.0"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tessa
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: '2.0'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Powell
|
@@ -9,22 +9,8 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2023-
|
12
|
+
date: 2023-02-17 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
-
- !ruby/object:Gem::Dependency
|
15
|
-
name: faraday
|
16
|
-
requirement: !ruby/object:Gem::Requirement
|
17
|
-
requirements:
|
18
|
-
- - ">="
|
19
|
-
- !ruby/object:Gem::Version
|
20
|
-
version: '0'
|
21
|
-
type: :runtime
|
22
|
-
prerelease: false
|
23
|
-
version_requirements: !ruby/object:Gem::Requirement
|
24
|
-
requirements:
|
25
|
-
- - ">="
|
26
|
-
- !ruby/object:Gem::Version
|
27
|
-
version: '0'
|
28
14
|
- !ruby/object:Gem::Dependency
|
29
15
|
name: virtus
|
30
16
|
requirement: !ruby/object:Gem::Requirement
|
@@ -151,6 +137,7 @@ files:
|
|
151
137
|
- app/javascript/tessa/index.js.coffee
|
152
138
|
- bin/rspec
|
153
139
|
- config/routes.rb
|
140
|
+
- docs/tessa-activestorage-sequence-diagram.drawio.png
|
154
141
|
- lib/tasks/tessa.rake
|
155
142
|
- lib/tessa.rb
|
156
143
|
- lib/tessa/active_storage/asset_wrapper.rb
|
@@ -161,6 +148,7 @@ files:
|
|
161
148
|
- lib/tessa/config.rb
|
162
149
|
- lib/tessa/controller_helpers.rb
|
163
150
|
- lib/tessa/engine.rb
|
151
|
+
- lib/tessa/fake_connection.rb
|
164
152
|
- lib/tessa/jobs/migrate_assets_job.rb
|
165
153
|
- lib/tessa/model.rb
|
166
154
|
- lib/tessa/model/dynamic_extensions.rb
|
@@ -250,6 +238,7 @@ files:
|
|
250
238
|
- spec/tessa/rack_upload_proxy_spec.rb
|
251
239
|
- spec/tessa/upload/uploads_file_spec.rb
|
252
240
|
- spec/tessa/upload_spec.rb
|
241
|
+
- spec/tessa_spec.rb
|
253
242
|
- tessa.gemspec
|
254
243
|
- yarn.lock
|
255
244
|
homepage: https://github.com/watermarkchurch/tessa-client
|
@@ -351,3 +340,4 @@ test_files:
|
|
351
340
|
- spec/tessa/rack_upload_proxy_spec.rb
|
352
341
|
- spec/tessa/upload/uploads_file_spec.rb
|
353
342
|
- spec/tessa/upload_spec.rb
|
343
|
+
- spec/tessa_spec.rb
|