Skip to content

Commit 39de455

Browse files
sarafordace-n
andauthored
feat(functions/v2): update imagemagick sample (GoogleCloudPlatform#2650)
* updated imagemagick sample to v2 * Add testing layer * Fix lint * Lint take 2 * Add supertest * Add zombie image Co-authored-by: ace-n <anassri@google.com>
1 parent 3294c1f commit 39de455

File tree

5 files changed

+188
-83
lines changed

5 files changed

+188
-83
lines changed

functions/imagemagick/index.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
'use strict';
1616

1717
// [START functions_imagemagick_setup]
18+
const functions = require('@google-cloud/functions-framework');
1819
const gm = require('gm').subClass({imageMagick: true});
1920
const fs = require('fs').promises;
2021
const path = require('path');
@@ -29,12 +30,12 @@ const {BLURRED_BUCKET_NAME} = process.env;
2930

3031
// [START functions_imagemagick_analyze]
3132
// Blurs uploaded images that are flagged as Adult or Violence.
32-
exports.blurOffensiveImages = async event => {
33+
functions.cloudEvent('blurOffensiveImages', async cloudEvent => {
3334
// This event represents the triggering Cloud Storage object.
34-
const object = event;
35-
36-
const file = storage.bucket(object.bucket).file(object.name);
37-
const filePath = `gs://${object.bucket}/${object.name}`;
35+
const bucket = cloudEvent.data.bucket;
36+
const name = cloudEvent.data.name;
37+
const file = storage.bucket(bucket).file(name);
38+
const filePath = `gs://${bucket}/${name}`;
3839

3940
console.log(`Analyzing ${file.name}.`);
4041

@@ -56,7 +57,7 @@ exports.blurOffensiveImages = async event => {
5657
console.error(`Failed to analyze ${file.name}.`, err);
5758
throw err;
5859
}
59-
};
60+
});
6061
// [END functions_imagemagick_analyze]
6162

6263
// [START functions_imagemagick_blur]

functions/imagemagick/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,15 @@
1515
"test": "mocha test/*.test.js --timeout=20000 --exit"
1616
},
1717
"dependencies": {
18+
"@google-cloud/functions-framework": "^3.1.0",
1819
"@google-cloud/storage": "^5.0.0",
1920
"@google-cloud/vision": "^2.0.0",
2021
"gm": "^1.23.1"
2122
},
2223
"devDependencies": {
23-
"@google-cloud/functions-framework": "^3.0.0",
24-
"gaxios": "^4.3.0",
2524
"mocha": "^9.0.0",
2625
"proxyquire": "^2.1.0",
2726
"sinon": "^13.0.0",
28-
"wait-port": "^0.2.9"
27+
"supertest": "^6.2.3"
2928
}
3029
}

functions/imagemagick/test/index.test.js renamed to functions/imagemagick/test/integration.test.js

Lines changed: 51 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,10 @@
1515
'use strict';
1616

1717
const assert = require('assert');
18-
const {spawn} = require('child_process');
1918
const {Storage} = require('@google-cloud/storage');
2019
const sinon = require('sinon');
21-
const {request} = require('gaxios');
22-
const waitPort = require('wait-port');
20+
const supertest = require('supertest');
21+
const functionsFramework = require('@google-cloud/functions-framework/testing');
2322

2423
const storage = new Storage();
2524

@@ -28,31 +27,11 @@ const {BLURRED_BUCKET_NAME} = process.env;
2827
const blurredBucket = storage.bucket(BLURRED_BUCKET_NAME);
2928

3029
const testFiles = {
31-
safe: 'bicycle.jpg',
30+
safe: 'wakeupcat.jpg',
3231
offensive: 'zombie.jpg',
3332
};
3433

35-
async function startFF(port) {
36-
const ffProc = spawn('npx', [
37-
'functions-framework',
38-
'--target',
39-
'blurOffensiveImages',
40-
'--signature-type',
41-
'event',
42-
'--port',
43-
port,
44-
]);
45-
const ffProcHandler = new Promise((resolve, reject) => {
46-
let stdout = '';
47-
let stderr = '';
48-
ffProc.stdout.on('data', data => (stdout += data));
49-
ffProc.stderr.on('data', data => (stderr += data));
50-
ffProc.on('error', reject);
51-
ffProc.on('exit', c => (c === 0 ? resolve(stdout) : reject(stderr)));
52-
});
53-
await waitPort({host: 'localhost', port});
54-
return {ffProc, ffProcHandler};
55-
}
34+
require('../index');
5635

5736
describe('functions/imagemagick tests', () => {
5837
before(async () => {
@@ -72,50 +51,48 @@ describe('functions/imagemagick tests', () => {
7251
}
7352
});
7453

75-
beforeEach(() => sinon.stub(console, 'error'));
76-
afterEach(() => console.error.restore());
54+
beforeEach(() => sinon.stub(console, 'log'));
55+
afterEach(() => console.log.restore());
7756

7857
describe('functions_imagemagick_setup functions_imagemagick_analyze functions_imagemagick_blur', () => {
7958
it('blurOffensiveImages detects safe images using Cloud Vision', async () => {
80-
const PORT = 8080;
81-
const {ffProc, ffProcHandler} = await startFF(PORT);
82-
83-
await request({
84-
url: `http://localhost:${PORT}/blurOffensiveImages`,
85-
method: 'POST',
59+
const event = {
60+
id: '1234',
61+
type: 'mock-gcs-event',
8662
data: {
87-
data: {
88-
bucket: BUCKET_NAME,
89-
name: testFiles.safe,
90-
},
63+
bucket: BUCKET_NAME,
64+
name: testFiles.safe,
9165
},
92-
});
93-
ffProc.kill();
94-
const stdout = await ffProcHandler;
95-
assert.ok(stdout.includes(`Detected ${testFiles.safe} as OK.`));
66+
};
67+
68+
const server = functionsFramework.getTestServer('blurOffensiveImages');
69+
await supertest(server)
70+
.post('/')
71+
.send(event)
72+
.set('Content-Type', 'application/json')
73+
.expect(204);
9674
});
9775

9876
it('blurOffensiveImages successfully blurs offensive images', async () => {
99-
const PORT = 8081;
100-
const {ffProc, ffProcHandler} = await startFF(PORT);
101-
102-
await request({
103-
url: `http://localhost:${PORT}/blurOffensiveImages`,
104-
method: 'POST',
77+
const event = {
78+
id: '1234',
79+
type: 'mock-gcs-event',
10580
data: {
106-
data: {
107-
bucket: BUCKET_NAME,
108-
name: testFiles.offensive,
109-
},
81+
bucket: BUCKET_NAME,
82+
name: testFiles.offensive,
11083
},
111-
});
112-
113-
ffProc.kill();
114-
const stdout = await ffProcHandler;
115-
116-
assert.ok(stdout.includes(`Blurred image: ${testFiles.offensive}`));
117-
assert.ok(
118-
stdout.includes(
84+
};
85+
86+
const server = functionsFramework.getTestServer('blurOffensiveImages');
87+
await supertest(server)
88+
.post('/')
89+
.send(event)
90+
.set('Content-Type', 'application/json')
91+
.expect(204);
92+
93+
assert(console.log.calledWith(`Blurred image: ${testFiles.offensive}`));
94+
assert(
95+
console.log.calledWith(
11996
`Uploaded blurred image to: gs://${BLURRED_BUCKET_NAME}/${testFiles.offensive}`
12097
)
12198
);
@@ -128,32 +105,32 @@ describe('functions/imagemagick tests', () => {
128105
});
129106

130107
it('blurOffensiveImages detects missing images as safe using Cloud Vision', async () => {
131-
const PORT = 8082;
132-
const {ffProc, ffProcHandler} = await startFF(PORT);
133108
const missingFileName = 'file-does-not-exist.jpg';
134-
135-
await request({
136-
url: `http://localhost:${PORT}/blurOffensiveImages`,
137-
method: 'POST',
109+
const event = {
110+
id: '1234',
111+
type: 'mock-gcs-event',
138112
data: {
139-
data: {
140-
bucket: BUCKET_NAME,
141-
name: missingFileName,
142-
},
113+
bucket: BUCKET_NAME,
114+
name: missingFileName,
143115
},
144-
});
116+
};
117+
118+
const server = functionsFramework.getTestServer('blurOffensiveImages');
119+
await supertest(server)
120+
.post('/')
121+
.send(event)
122+
.set('Content-Type', 'application/json')
123+
.expect(204);
145124

146-
ffProc.kill();
147-
const stdout = await ffProcHandler;
148-
assert.ok(stdout.includes(`Detected ${missingFileName} as OK.`));
125+
assert(console.log.calledWith(`Detected ${missingFileName} as OK.`));
149126
});
150127
});
151128

152129
after(async () => {
153130
try {
154131
await blurredBucket.file(testFiles.offensive).delete();
155132
} catch (err) {
156-
console.log('Error deleting uploaded file:', err.message);
133+
console.error('Error deleting uploaded file:', err.message);
157134
}
158135
});
159136
});
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
const proxyquire = require('proxyquire');
16+
const sinon = require('sinon');
17+
18+
const {getFunction} = require('@google-cloud/functions-framework/testing');
19+
20+
const loadSample = (adultResult, fileName) => {
21+
const vision = sinon.stub();
22+
vision.ImageAnnotatorClient = function client() {
23+
return {
24+
safeSearchDetection: () => {
25+
return [
26+
{
27+
safeSearchAnnotation: {
28+
adult: adultResult,
29+
},
30+
},
31+
];
32+
},
33+
};
34+
};
35+
36+
const storage = {
37+
Storage: function client() {
38+
return {
39+
bucket: sinon.stub().returnsThis(),
40+
file: sinon.stub().returnsThis(),
41+
upload: sinon.stub().returnsThis(),
42+
download: sinon.stub().returnsThis(),
43+
name: fileName,
44+
};
45+
},
46+
};
47+
48+
const gm = () => {
49+
return {
50+
blur: sinon.stub().returnsThis(),
51+
write: sinon.stub().yields(),
52+
};
53+
};
54+
gm.subClass = sinon.stub().returnsThis();
55+
56+
const fs = {
57+
promises: {
58+
unlink: sinon.stub(),
59+
},
60+
};
61+
62+
return proxyquire('..', {
63+
'@google-cloud/vision': vision,
64+
'@google-cloud/storage': storage,
65+
gm: gm,
66+
fs: fs,
67+
});
68+
};
69+
70+
describe('functions_imagemagick_setup functions_imagemagick_analyze functions_imagemagick_blur', () => {
71+
const assert = require('assert');
72+
const sinon = require('sinon');
73+
74+
const stubConsole = function () {
75+
sinon.stub(console, 'log');
76+
sinon.stub(console, 'error');
77+
};
78+
79+
const restoreConsole = function () {
80+
console.log.restore();
81+
console.error.restore();
82+
};
83+
84+
beforeEach(stubConsole);
85+
afterEach(restoreConsole);
86+
87+
it('blurOffensiveImages marks safe images OK', async () => {
88+
const fileName = 'safe.jpg';
89+
loadSample('UNLIKELY', fileName);
90+
91+
const event = {
92+
id: '1234',
93+
type: 'mock-gcs-event',
94+
data: {
95+
bucket: 'my-bucket',
96+
name: 'safe.jpg',
97+
},
98+
};
99+
100+
// Call tested function and verify its behavior
101+
const blurOffensiveImages = getFunction('blurOffensiveImages');
102+
103+
await blurOffensiveImages(event);
104+
105+
assert(console.log.calledWith('Detected safe.jpg as OK.'));
106+
});
107+
108+
it('blurOffensiveImages successfully blurs offensive images', async () => {
109+
const fileName = 'offensive.jpg';
110+
loadSample('VERY_LIKELY', fileName);
111+
112+
const event = {
113+
id: '1234',
114+
type: 'mock-gcs-event',
115+
data: {
116+
bucket: 'my-bucket',
117+
name: fileName,
118+
},
119+
};
120+
121+
// Call tested function and verify its behavior
122+
const blurOffensiveImages = getFunction('blurOffensiveImages');
123+
124+
await blurOffensiveImages(event);
125+
126+
assert(console.log.calledWith('Detected offensive.jpg as inappropriate.'));
127+
});
128+
});

functions/imagemagick/zombie.jpg

191 KB
Loading

0 commit comments

Comments
 (0)