tessa 1.2.0 → 1.2.2
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 +0 -1
- data/README.md +3 -77
- data/lib/tasks/tessa.rake +1 -12
- data/lib/tessa/config.rb +9 -1
- data/lib/tessa/jobs/migrate_assets_job.rb +16 -14
- data/lib/tessa/model/dynamic_extensions.rb +22 -5
- data/lib/tessa/model.rb +14 -4
- data/lib/tessa/upload/uploads_file.rb +3 -1
- data/lib/tessa/version.rb +1 -1
- data/lib/tessa.rb +1 -1
- data/spec/support/remote_call_macro.rb +6 -1
- data/spec/tessa/config_spec.rb +42 -0
- data/spec/tessa/model_spec.rb +259 -6
- data/tessa.gemspec +1 -0
- metadata +16 -6
- data/docs/tessa-activestorage-sequence-diagram.drawio.png +0 -0
- data/lib/tessa/fake_connection.rb +0 -29
- data/spec/tessa_spec.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 78e7edfe1685678714fdec2e58b81f476dc33be7c2affa35acf78a5375da25e1
|
4
|
+
data.tar.gz: aafa43aa6aae2012b1201b97ec1a29c6f35435f8426622c50ba3f9c8e8438546
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e8448afb3467983a468e65b16bbda79c028c1e2a9d197d1fce48b6ade6fb5aedb8b1d0597fc823d133552eb206146ee7c6ebf7be0dcecc1bddc12cc0dd46d2d4
|
7
|
+
data.tar.gz: c61dc18d7bfc8b435ae760e1d8357aecddbdcb8122896bea63b636659ad76fc2bd1333214196a067928bd3b10da97d43cde22b7bcb400e153bb778ea683edc1d
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,27 +1,11 @@
|
|
1
|
-
# [Tessa
|
1
|
+
# [Tessa][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
|
-
|
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.
|
8
|
+
Manage your assets.
|
25
9
|
|
26
10
|
## Installation
|
27
11
|
|
@@ -41,65 +25,7 @@ Or install it yourself as:
|
|
41
25
|
|
42
26
|
## Usage
|
43
27
|
|
44
|
-
|
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.
|
28
|
+
TODO: Write usage instructions here
|
103
29
|
|
104
30
|
## Contributing
|
105
31
|
|
data/lib/tasks/tessa.rake
CHANGED
@@ -2,17 +2,6 @@
|
|
2
2
|
namespace :tessa do
|
3
3
|
desc "Begins the migration of all Tessa assets to ActiveStorage."
|
4
4
|
task :migrate => :environment do
|
5
|
-
|
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
|
5
|
+
Tessa::MigrateAssetsJob.perform_later
|
17
6
|
end
|
18
7
|
end
|
data/lib/tessa/config.rb
CHANGED
@@ -10,7 +10,15 @@ module Tessa
|
|
10
10
|
attribute :strategy, String, default: -> (*_) { ENV['TESSA_STRATEGY'] || DEFAULT_STRATEGY }
|
11
11
|
|
12
12
|
def connection
|
13
|
-
@connection ||=
|
13
|
+
@connection ||= Faraday.new(url: url) do |conn|
|
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
|
14
22
|
end
|
15
23
|
end
|
16
24
|
end
|
@@ -72,6 +72,14 @@ 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
|
75
83
|
rescue StandardError => ex
|
76
84
|
Rails.logger.error("#{record.class}#{record.id}##{field_state.field_name}: error - #{ex}")
|
77
85
|
field_state.offset += 1
|
@@ -127,24 +135,18 @@ class Tessa::MigrateAssetsJob < ActiveJob::Base
|
|
127
135
|
end
|
128
136
|
|
129
137
|
def load_models_from_registry
|
130
|
-
|
131
|
-
|
132
|
-
|
138
|
+
Rails.application.eager_load!
|
139
|
+
|
140
|
+
# Load all Tessa models that can have attachments (not form objects)
|
141
|
+
models = Tessa.model_registry
|
142
|
+
.select { |m| m.respond_to?(:has_one_attached) }
|
133
143
|
|
134
|
-
|
135
|
-
|
136
|
-
def self.complete?
|
137
|
-
ProcessingState.initialize_from_models.fully_processed?
|
144
|
+
# Initialize our Record Keeping object
|
145
|
+
ProcessingState.initialize_from_models(models)
|
138
146
|
end
|
139
147
|
|
140
148
|
ProcessingState = Struct.new(:model_queue, :batch_count) do
|
141
|
-
def self.initialize_from_models(models
|
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
|
-
|
149
|
+
def self.initialize_from_models(models)
|
148
150
|
new(
|
149
151
|
models.map do |model|
|
150
152
|
ModelProcessingState.initialize_from_model(model)
|
@@ -22,11 +22,19 @@ 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
|
25
30
|
end
|
26
31
|
|
27
32
|
def #{field.id_field}
|
28
33
|
# Use the attachment's key
|
29
34
|
return #{name}_attachment.key if #{name}_attachment.present?
|
35
|
+
|
36
|
+
# fallback to Tessa's database column
|
37
|
+
super
|
30
38
|
end
|
31
39
|
|
32
40
|
def #{name}=(attachable)
|
@@ -72,6 +80,7 @@ class Tessa::DynamicExtensions
|
|
72
80
|
|
73
81
|
@#{name} ||= [
|
74
82
|
*#{name}_attachments.map { |a| Tessa::ActiveStorage::AssetWrapper.new(a) },
|
83
|
+
*fetch_tessa_remote_assets(tessa_ids)
|
75
84
|
]
|
76
85
|
end
|
77
86
|
|
@@ -79,6 +88,8 @@ class Tessa::DynamicExtensions
|
|
79
88
|
[
|
80
89
|
# Use the attachment's key
|
81
90
|
*#{name}_attachments.map(&:key),
|
91
|
+
# include from Tessa's database column
|
92
|
+
*super
|
82
93
|
]
|
83
94
|
end
|
84
95
|
|
@@ -94,8 +105,8 @@ class Tessa::DynamicExtensions
|
|
94
105
|
existing.destroy
|
95
106
|
else
|
96
107
|
ids = self.#{field.id_field}
|
97
|
-
ids
|
98
|
-
self.#{field.id_field} = ids
|
108
|
+
ids.delete(change.id.to_i)
|
109
|
+
self.#{field.id_field} = ids.any? ? ids : nil
|
99
110
|
end
|
100
111
|
end
|
101
112
|
attachables.changes.select(&:add?).each do |change|
|
@@ -127,7 +138,10 @@ class Tessa::DynamicExtensions
|
|
127
138
|
def build(mod)
|
128
139
|
mod.class_eval <<~CODE, __FILE__, __LINE__ + 1
|
129
140
|
attr_accessor :#{name}_id
|
130
|
-
|
141
|
+
attr_writer :#{name}
|
142
|
+
def #{name}
|
143
|
+
@#{name} ||= fetch_tessa_remote_assets(#{name}_id)
|
144
|
+
end
|
131
145
|
CODE
|
132
146
|
mod
|
133
147
|
end
|
@@ -137,9 +151,12 @@ class Tessa::DynamicExtensions
|
|
137
151
|
def build(mod)
|
138
152
|
mod.class_eval <<~CODE, __FILE__, __LINE__ + 1
|
139
153
|
attr_accessor :#{name}_ids
|
140
|
-
|
154
|
+
attr_writer :#{name}
|
155
|
+
def #{name}
|
156
|
+
@#{name} ||= fetch_tessa_remote_assets(#{name}_ids)
|
157
|
+
end
|
141
158
|
CODE
|
142
159
|
mod
|
143
160
|
end
|
144
161
|
end
|
145
|
-
end
|
162
|
+
end
|
data/lib/tessa/model.rb
CHANGED
@@ -8,6 +8,8 @@ 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)
|
11
13
|
|
12
14
|
Tessa.model_registry << base
|
13
15
|
end
|
@@ -19,9 +21,9 @@ module Tessa
|
|
19
21
|
end
|
20
22
|
|
21
23
|
def apply_tessa_change_sets
|
22
|
-
|
23
|
-
|
24
|
-
|
24
|
+
pending_tessa_change_sets.delete_if do |_, change_set|
|
25
|
+
change_set.apply
|
26
|
+
end
|
25
27
|
end
|
26
28
|
|
27
29
|
def remove_all_tessa_assets
|
@@ -35,9 +37,17 @@ module Tessa
|
|
35
37
|
end
|
36
38
|
|
37
39
|
def fetch_tessa_remote_assets(ids)
|
38
|
-
# This should just always return Tessa::AssetFailure
|
39
40
|
Tessa.find_assets(ids)
|
40
41
|
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
|
41
51
|
end
|
42
52
|
|
43
53
|
module ClassMethods
|
data/lib/tessa/version.rb
CHANGED
data/lib/tessa.rb
CHANGED
@@ -24,11 +24,16 @@ 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(:
|
27
|
+
let(:faraday_stubs) {
|
28
|
+
Faraday::Adapter::Test::Stubs.new do |stub|
|
29
|
+
stub.send(method, path) { |env| [422, {}, { "error" => "error" }.to_json] }
|
30
|
+
end
|
31
|
+
}
|
28
32
|
|
29
33
|
it "raises Tessa::RequestFailed" do
|
30
34
|
expect{ call }.to raise_error { |error|
|
31
35
|
expect(error).to be_a(Tessa::RequestFailed)
|
36
|
+
expect(error.response).to be_a(Faraday::Response)
|
32
37
|
}
|
33
38
|
end
|
34
39
|
end
|
data/spec/tessa/config_spec.rb
CHANGED
@@ -67,4 +67,46 @@ 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
|
70
112
|
end
|
data/spec/tessa/model_spec.rb
CHANGED
@@ -93,10 +93,17 @@ RSpec.describe Tessa::Model do
|
|
93
93
|
let(:instance) { model.new(another_place: []) }
|
94
94
|
subject(:getter) { instance.multiple_field }
|
95
95
|
|
96
|
-
it "
|
96
|
+
it "calls find for each of the file_ids and returns result" do
|
97
97
|
instance.another_place = [1, 2, 3]
|
98
|
-
expect(Tessa::Asset).
|
99
|
-
expect(getter).to eq([])
|
98
|
+
expect(Tessa::Asset).to receive(:find).with([1, 2, 3]).and_return([:a1, :a2, :a3])
|
99
|
+
expect(getter).to eq([:a1, :a2, :a3])
|
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
|
100
107
|
end
|
101
108
|
|
102
109
|
context "with no values" do
|
@@ -114,10 +121,17 @@ RSpec.describe Tessa::Model do
|
|
114
121
|
}
|
115
122
|
subject(:getter) { instance.avatar }
|
116
123
|
|
117
|
-
it "
|
124
|
+
it "calls find for file_id and returns result" do
|
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
|
118
131
|
instance.avatar_id = 1
|
119
|
-
expect(Tessa::Asset).
|
120
|
-
|
132
|
+
expect(Tessa::Asset).to receive(:find).and_return(:val).once
|
133
|
+
instance.avatar
|
134
|
+
instance.avatar
|
121
135
|
end
|
122
136
|
|
123
137
|
it "wraps ActiveStorage uploads with AssetWrapper" do
|
@@ -320,6 +334,245 @@ RSpec.describe Tessa::Model do
|
|
320
334
|
|
321
335
|
expect(instance.another_place).to eq(keys)
|
322
336
|
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
|
323
425
|
end
|
324
426
|
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
|
+
|
325
578
|
end
|
data/tessa.gemspec
CHANGED
@@ -23,6 +23,7 @@ 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"
|
26
27
|
spec.add_dependency "virtus", "~>1.0.4"
|
27
28
|
|
28
29
|
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: 1.2.
|
4
|
+
version: 1.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Powell
|
@@ -9,8 +9,22 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2023-
|
12
|
+
date: 2023-03-20 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'
|
14
28
|
- !ruby/object:Gem::Dependency
|
15
29
|
name: virtus
|
16
30
|
requirement: !ruby/object:Gem::Requirement
|
@@ -137,7 +151,6 @@ files:
|
|
137
151
|
- app/javascript/tessa/index.js.coffee
|
138
152
|
- bin/rspec
|
139
153
|
- config/routes.rb
|
140
|
-
- docs/tessa-activestorage-sequence-diagram.drawio.png
|
141
154
|
- lib/tasks/tessa.rake
|
142
155
|
- lib/tessa.rb
|
143
156
|
- lib/tessa/active_storage/asset_wrapper.rb
|
@@ -148,7 +161,6 @@ files:
|
|
148
161
|
- lib/tessa/config.rb
|
149
162
|
- lib/tessa/controller_helpers.rb
|
150
163
|
- lib/tessa/engine.rb
|
151
|
-
- lib/tessa/fake_connection.rb
|
152
164
|
- lib/tessa/jobs/migrate_assets_job.rb
|
153
165
|
- lib/tessa/model.rb
|
154
166
|
- lib/tessa/model/dynamic_extensions.rb
|
@@ -238,7 +250,6 @@ files:
|
|
238
250
|
- spec/tessa/rack_upload_proxy_spec.rb
|
239
251
|
- spec/tessa/upload/uploads_file_spec.rb
|
240
252
|
- spec/tessa/upload_spec.rb
|
241
|
-
- spec/tessa_spec.rb
|
242
253
|
- tessa.gemspec
|
243
254
|
- yarn.lock
|
244
255
|
homepage: https://github.com/watermarkchurch/tessa-client
|
@@ -340,4 +351,3 @@ test_files:
|
|
340
351
|
- spec/tessa/rack_upload_proxy_spec.rb
|
341
352
|
- spec/tessa/upload/uploads_file_spec.rb
|
342
353
|
- spec/tessa/upload_spec.rb
|
343
|
-
- spec/tessa_spec.rb
|
Binary file
|
@@ -1,29 +0,0 @@
|
|
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
|
data/spec/tessa_spec.rb
DELETED
@@ -1,23 +0,0 @@
|
|
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
|