Skip to content

Commit 6ae3fd7

Browse files
committed
update tests for sb3 import functionality
1 parent 5ec7c72 commit 6ae3fd7

3 files changed

Lines changed: 270 additions & 0 deletions

File tree

spec/jobs/upload_job_spec.rb

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,116 @@
269269
end
270270
end
271271

272+
context 'when a scratch project is uploaded' do
273+
let(:scratch_payload) do
274+
{
275+
repository: { name: 'my-amazing-repo', owner: { name: 'me' } },
276+
commits: [{ added: ['ja-JP/code/scratch-integration-test-starter/main.sb3'], modified: [], removed: [] }]
277+
}
278+
end
279+
let(:scratch_project_json) do
280+
{
281+
targets: [
282+
{
283+
costumes: [{ md5ext: 'test_image_1.png' }],
284+
sounds: [{ md5ext: 'test_audio_1.mp3' }]
285+
}
286+
]
287+
}
288+
end
289+
let(:scratch_sb3_body) do
290+
sb3_archive_string(
291+
'project.json' => scratch_project_json.to_json,
292+
'test_image_1.png' => sb3_fixture_content('test_image_1.png'),
293+
'test_audio_1.mp3' => sb3_fixture_content('test_audio_1.mp3')
294+
)
295+
end
296+
let(:raw_response) do
297+
{
298+
data: {
299+
repository: {
300+
object: {
301+
__typename: 'Tree',
302+
entries: [
303+
{
304+
name: 'scratch-integration-test-starter',
305+
object: {
306+
__typename: 'Tree',
307+
entries: [
308+
{
309+
name: 'main.sb3',
310+
extension: '.sb3',
311+
object: {
312+
__typename: 'Blob',
313+
text: nil,
314+
isBinary: true
315+
}
316+
},
317+
{
318+
name: 'project_config.yml',
319+
extension: '.yml',
320+
object: {
321+
__typename: 'Blob',
322+
text: "name: \"Scratch Integration Test\"\nidentifier: \"scratch-integration-test-starter\"\ntype: \"code_editor_scratch\"\n",
323+
isBinary: false
324+
}
325+
}
326+
]
327+
}
328+
}
329+
]
330+
}
331+
}
332+
}
333+
}.deep_stringify_keys
334+
end
335+
336+
before do
337+
allow(GithubApi::Client).to receive(:query).and_return(graphql_response)
338+
allow(ProjectImporter).to receive(:new).and_call_original
339+
340+
stub_request(:get, 'https://github.com/me/my-amazing-repo/raw/branches/whatever/ja-JP/code/scratch-integration-test-starter/main.sb3')
341+
.to_return(status: 200, body: scratch_sb3_body, headers: {})
342+
end
343+
344+
it 'imports the Scratch project with the sb3 component as io' do
345+
described_class.perform_now(scratch_payload)
346+
347+
expect(ProjectImporter).to have_received(:new).with(
348+
hash_including(
349+
name: 'Scratch Integration Test',
350+
identifier: 'scratch-integration-test-starter',
351+
type: Project::Types::CODE_EDITOR_SCRATCH,
352+
locale: 'ja-JP',
353+
images: [],
354+
videos: [],
355+
audio: [],
356+
components: [
357+
hash_including(
358+
name: 'main',
359+
extension: 'sb3',
360+
io: an_object_responding_to(:read)
361+
)
362+
]
363+
)
364+
)
365+
end
366+
367+
it 'requests the sb3 file from the correct URL' do
368+
described_class.perform_now(scratch_payload)
369+
370+
expect(WebMock).to have_requested(:get, 'https://github.com/me/my-amazing-repo/raw/branches/whatever/ja-JP/code/scratch-integration-test-starter/main.sb3').once
371+
end
372+
373+
it 'saves the Scratch project to the database' do
374+
expect { described_class.perform_now(scratch_payload) }.to change(Project, :count).by(1)
375+
376+
project = Project.find_by(identifier: 'scratch-integration-test-starter', locale: 'ja-JP')
377+
expect(project.project_type).to eq(Project::Types::CODE_EDITOR_SCRATCH)
378+
expect(project.scratch_component.content).to eq(JSON.parse(scratch_project_json.to_json))
379+
end
380+
end
381+
272382
context 'when locale is unsupported' do
273383
let(:raw_response) { { data: { repository: nil } } }
274384
let(:bad_payload) do

spec/lib/project_importer_spec.rb

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,105 @@
114114
expect { importer.import! }.to change { project.reload.audio[0].filename.to_s }.to('my-amazing-audio.mp3')
115115
end
116116
end
117+
118+
context 'when the project has type code_editor_scratch' do
119+
let(:scratch_project_file) { Tempfile.new(['test_scratch_project', '.sb3']) }
120+
let(:importer) do
121+
described_class.new(
122+
name: 'My amazing Scratch project',
123+
identifier: 'my-amazing-scratch-project',
124+
type: Project::Types::CODE_EDITOR_SCRATCH,
125+
locale: 'en',
126+
components: [
127+
{ name: 'main', extension: 'sb3', file_path: scratch_project_file.path }
128+
]
129+
)
130+
end
131+
let(:scratch_project_content) do
132+
{
133+
targets: [
134+
{
135+
costumes: [{ md5ext: 'test_image_1.png' }],
136+
sounds: [{ md5ext: 'test_audio_1.mp3' }],
137+
videos: [{ md5ext: 'test_video_1.mp4' }]
138+
}
139+
]
140+
}
141+
end
142+
143+
let(:project) { Project.find_by(identifier: importer.identifier, user_id: nil, locale: importer.locale) }
144+
145+
before do
146+
scratch_project_file.binmode
147+
scratch_project_file.write(
148+
sb3_archive(
149+
'project.json' => scratch_project_content.to_json,
150+
'test_image_1.png' => sb3_fixture_content('test_image_1.png'),
151+
'test_video_1.mp4' => sb3_fixture_content('test_video_1.mp4'),
152+
'test_audio_1.mp3' => sb3_fixture_content('test_audio_1.mp3')
153+
).read
154+
)
155+
scratch_project_file.flush
156+
end
157+
158+
after do
159+
scratch_project_file.close
160+
scratch_project_file.unlink
161+
end
162+
163+
context 'when importing a new scratch project' do
164+
it 'imports the Scratch component content' do
165+
importer.import!
166+
167+
expect(project.components.count).to eq(0)
168+
expect(project.scratch_component.content).to eq(JSON.parse(scratch_project_content.to_json))
169+
end
170+
171+
it 'imports the project assets' do
172+
importer.import!
173+
expect(ScratchAsset.global_assets.where(filename: ['test_image_1.png', 'test_video_1.mp4', 'test_audio_1.mp3']).count).to eq(3)
174+
end
175+
end
176+
177+
context 'when the scratch project already exists in the database' do
178+
let(:original_scratch_content) do
179+
{ targets: ['old target'], monitors: [], extensions: [], meta: {} }
180+
end
181+
let!(:project) do
182+
create(
183+
:project,
184+
identifier: 'my-amazing-scratch-project',
185+
locale: 'en',
186+
project_type: Project::Types::CODE_EDITOR_SCRATCH,
187+
name: 'Old Scratch project name'
188+
)
189+
end
190+
191+
before do
192+
create(:scratch_component, project:, content: original_scratch_content)
193+
end
194+
195+
it 'does not create a new project' do
196+
expect { importer.import! }.not_to change(Project, :count)
197+
end
198+
199+
it 'updates the project name' do
200+
expect { importer.import! }.to change { project.reload.name }.to(importer.name)
201+
end
202+
203+
it 'updates the scratch component content' do
204+
importer.import!
205+
206+
expect(project.reload.scratch_component.content).to eq(JSON.parse(scratch_project_content.to_json))
207+
end
208+
209+
it 'imports any new assets without duplicating existing ones' do
210+
create(:scratch_asset, :with_file, filename: 'test_image_1.png')
211+
212+
importer.import!
213+
214+
expect(ScratchAsset.global_assets.where(filename: ['test_image_1.png', 'test_video_1.mp4', 'test_audio_1.mp3']).count).to eq(3)
215+
end
216+
end
217+
end
117218
end

spec/lib/scratch_asset_importer_spec.rb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,63 @@
118118
end
119119
end
120120
end
121+
122+
describe '.import_all_from_sb3' do
123+
def sb3_asset(filename, content = sb3_fixture_content(filename))
124+
{ filename:, io: StringIO.new(content) }
125+
end
126+
127+
it 'imports assets from SB3 archive content' do
128+
png_content = sb3_fixture_content('test_image_1.png')
129+
130+
described_class.import_all_from_sb3([sb3_asset('test_image_1.png', png_content)])
131+
132+
scratch_asset = ScratchAsset.find_by(filename: 'test_image_1.png')
133+
expect(scratch_asset).to be_present
134+
expect(scratch_asset).to be_global
135+
expect(scratch_asset.file.download).to eq(png_content)
136+
end
137+
138+
it 'does nothing if global asset already exists' do
139+
create(:scratch_asset, :with_file, filename: 'test_image_1.png')
140+
141+
expect do
142+
described_class.import_all_from_sb3([sb3_asset('test_image_1.png')])
143+
end.not_to change(ScratchAsset, :count)
144+
end
145+
146+
it 'can import multiple assets' do
147+
described_class.import_all_from_sb3([
148+
sb3_asset('test_image_1.png'),
149+
sb3_asset('test_audio_1.mp3', sb3_fixture_content('test_audio_1.mp3'))
150+
])
151+
152+
expect(ScratchAsset.find_by(filename: 'test_image_1.png')).to be_present
153+
expect(ScratchAsset.find_by(filename: 'test_audio_1.mp3')).to be_present
154+
end
155+
156+
it 'still imports a global asset when a project asset already uses the filename' do
157+
project = create(:project, project_type: Project::Types::CODE_EDITOR_SCRATCH, locale: nil, user_id: SecureRandom.uuid)
158+
create(:scratch_component, project:)
159+
create(:scratch_asset, :with_file, filename: 'test_image_1.png', project:)
160+
161+
expect do
162+
described_class.import_all_from_sb3([sb3_asset('test_image_1.png')])
163+
end.to change { ScratchAsset.global_assets.where(filename: 'test_image_1.png').count }.by(1)
164+
end
165+
166+
it 'skips assets that fail to import' do
167+
allow(ScratchAsset).to receive(:create!).and_call_original
168+
allow(ScratchAsset).to receive(:create!).with(filename: 'failing.png', project_id: nil, uploaded_user_id: nil)
169+
.and_raise(StandardError, 'attach failed')
170+
171+
described_class.import_all_from_sb3([
172+
sb3_asset('failing.png', 'bad'),
173+
sb3_asset('test_image_1.png')
174+
])
175+
176+
expect(ScratchAsset.find_by(filename: 'failing.png')).not_to be_present
177+
expect(ScratchAsset.find_by(filename: 'test_image_1.png')).to be_present
178+
end
179+
end
121180
end

0 commit comments

Comments
 (0)