Skip to content

Commit 52ff214

Browse files
author
Ace Nassri
authored
feat(functions): add firestore sample (GoogleCloudPlatform#1225)
1 parent abfc636 commit 52ff214

File tree

5 files changed

+371
-0
lines changed

5 files changed

+371
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"require": {
3+
"google/cloud-functions-framework": "^0.7.1",
4+
"google/cloud-firestore": "^1.18"
5+
},
6+
"scripts": {
7+
"start": [
8+
"Composer\\Config::disableProcessTimeout",
9+
"FUNCTION_SIGNATURE_TYPE=cloudevent FUNCTION_TARGET=firebaseFirestore php -S localhost:${PORT:-8080} vendor/bin/router.php"
10+
]
11+
},
12+
"require-dev": {
13+
"google/cloud-logging": "^1.21"
14+
}
15+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
2+
/**
3+
* Copyright 2020 Google LLC.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
// [START functions_firebase_firestore]
19+
20+
use Google\CloudFunctions\CloudEvent;
21+
22+
function firebaseFirestore(CloudEvent $cloudevent)
23+
{
24+
$log = fopen(getenv('LOGGER_OUTPUT') ?: 'php://stderr', 'wb');
25+
26+
fwrite($log, "Event: " . $cloudevent->getId() . PHP_EOL);
27+
fwrite($log, "Event Type: " . $cloudevent->getType() . PHP_EOL);
28+
29+
$data = $cloudevent->getData();
30+
31+
$resource = $data['resource'];
32+
fwrite($log, 'Function triggered by event on: ' . $resource . PHP_EOL);
33+
34+
if (isset($data['oldValue'])) {
35+
fwrite($log, 'Old value: ' . json_encode($data['oldValue']) . PHP_EOL);
36+
}
37+
38+
if (isset($data['value'])) {
39+
fwrite($log, 'New value: ' . json_encode($data['value']) . PHP_EOL);
40+
}
41+
}
42+
// [END functions_firebase_firestore]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
xml version="1.0" encoding="UTF-8"?>
2+
17+
<phpunit bootstrap="../../testing/bootstrap.php" convertWarningsToExceptions="false">
18+
<testsuites>
19+
<testsuite name="Cloud Functions Firebase Firestore Test Suite">
20+
<directory>testdirectory>
21+
<exclude>vendorexclude>
22+
testsuite>
23+
testsuites>
24+
<logging>
25+
<log type="coverage-clover" target="build/logs/clover.xml"/>
26+
logging>
27+
<filter>
28+
<whitelist>
29+
<directory suffix=".php">.directory>
30+
<exclude>
31+
<directory>./vendordirectory>
32+
exclude>
33+
whitelist>
34+
filter>
35+
phpunit>
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
2+
/**
3+
* Copyright 2020 Google LLC.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace Google\Cloud\Samples\Functions\FirebaseFirestore\Test;
21+
22+
use Google\Cloud\Firestore\FirestoreClient;
23+
use Google\Cloud\Logging\LoggingClient;
24+
use Google\Cloud\TestUtils\CloudFunctionDeploymentTrait;
25+
use Google\Cloud\TestUtils\EventuallyConsistentTestTrait;
26+
use Google\Cloud\TestUtils\GcloudWrapper\CloudFunction;
27+
use PHPUnit\Framework\TestCase;
28+
use PHPUnit\Framework\ExpectationFailedException;
29+
30+
/**
31+
* Class DeployTest.
32+
*
33+
* This test is not run by the CI system.
34+
*
35+
* To skip deployment of a new function, run with "GOOGLE_SKIP_DEPLOYMENT=true".
36+
* To skip deletion of the tested function, run with "GOOGLE_KEEP_DEPLOYMENT=true".
37+
*/
38+
class DeployTest extends TestCase
39+
{
40+
use CloudFunctionDeploymentTrait;
41+
use EventuallyConsistentTestTrait;
42+
43+
/** @var string */
44+
private static $entryPoint = 'firebaseFirestore';
45+
46+
/** @var string */
47+
private static $functionSignatureType = 'cloudevent';
48+
49+
/** @var string */
50+
private static $collectionName = 'foods';
51+
52+
/** @var string */
53+
private static $documentName = 'taco';
54+
55+
/** @var LoggingClient */
56+
private static $loggingClient;
57+
58+
/** @var FirestoreClient */
59+
private static $firestoreClient;
60+
61+
/**
62+
* Deploy the Cloud Function, called from DeploymentTrait::deployApp().
63+
*
64+
* Overrides CloudFunctionDeploymentTrait::doDeploy().
65+
*/
66+
private static function doDeploy()
67+
{
68+
$project = self::requireOneOfEnv([
69+
'FIRESTORE_PROJECT_ID',
70+
'GOOGLE_PROJECT_ID'
71+
]);
72+
73+
$resource =
74+
'projects/' . $project . '/databases/(default)/documents/' . self::$collectionName . '/' . self::$documentName;
75+
$event = 'providers/cloud.firestore/eventTypes/document.write';
76+
77+
return self::$fn->deploy([
78+
'--trigger-resource' => $resource,
79+
'--trigger-event' => $event
80+
], '');
81+
}
82+
83+
public function dataProvider()
84+
{
85+
$data = uniqid();
86+
return [
87+
[
88+
'data' => ['flavor' => $data],
89+
'expected' => $data
90+
],
91+
];
92+
}
93+
94+
/**
95+
* @dataProvider dataProvider
96+
*/
97+
public function testFirebaseFirestore(array $data, string $expected): void
98+
{
99+
// Trigger storage upload.
100+
$objectUri = $this->updateFirestore(
101+
self::$collectionName,
102+
self::$documentName,
103+
$data
104+
);
105+
106+
// Give event and log systems a head start.
107+
// If log retrieval fails to find logs for our function within retry limit, increase sleep time.
108+
sleep(5);
109+
110+
$fiveMinAgo = date(\DateTime::RFC3339, strtotime('-5 minutes'));
111+
$this->processFunctionLogs(self::$fn, $fiveMinAgo, function (\Iterator $logs) use ($expected) {
112+
// Concatenate all relevant log messages.
113+
$actual = '';
114+
foreach ($logs as $log) {
115+
$info = $log->info();
116+
if (isset($info['textPayload'])) {
117+
$actual .= $info['textPayload'];
118+
}
119+
}
120+
121+
// Only testing one property to decrease odds the expected logs are
122+
// split between log requests.
123+
$this->assertContains($expected, $actual);
124+
});
125+
}
126+
127+
/**
128+
* Retrieve and process logs for the defined function.
129+
*
130+
* @param CloudFunction $fn function whose logs should be checked.
131+
* @param string $startTime RFC3339 timestamp marking start of time range to retrieve.
132+
* @param callable $process callback function to run on the logs.
133+
*/
134+
private function processFunctionLogs(CloudFunction $fn, string $startTime, callable $process)
135+
{
136+
$projectId = self::requireEnv('GOOGLE_PROJECT_ID');
137+
138+
if (empty(self::$loggingClient)) {
139+
self::$loggingClient = new LoggingClient([
140+
'projectId' => $projectId
141+
]);
142+
}
143+
144+
// Define the log search criteria.
145+
$logFullName = 'projects/' . $projectId . '/logs/cloudfunctions.googleapis.com%2Fcloud-functions';
146+
$filter = sprintf(
147+
'logName="%s" resource.labels.function_name="%s" timestamp>="%s"',
148+
$logFullName,
149+
$fn->getFunctionName(),
150+
$startTime
151+
);
152+
153+
echo "\nRetrieving logs [$filter]...\n";
154+
155+
// Check for new logs for the function.
156+
$attempt = 1;
157+
$this->runEventuallyConsistentTest(function () use ($filter, $process, &$attempt) {
158+
$entries = self::$loggingClient->entries(['filter' => $filter]);
159+
160+
// If no logs came in try again.
161+
if (empty($entries->current())) {
162+
echo 'Logs not found, attempting retry #' . $attempt++ . PHP_EOL;
163+
throw new ExpectationFailedException('Log Entries not available');
164+
}
165+
echo 'Processing logs...' . PHP_EOL;
166+
167+
$process($entries);
168+
}, $retries = 10);
169+
}
170+
171+
/**
172+
* Update a value in Firebase Realtime Database (RTDB).
173+
*
174+
* @param string $document The Firestore document to modify.
175+
* @param string $collection The Firestore collection to modify.
176+
* @param string $data The key-value pair to set the specified collection to.
177+
*
178+
* @throws \RuntimeException
179+
*/
180+
private function updateFirestore(
181+
string $document,
182+
string $collection,
183+
array $data
184+
): void {
185+
if (empty(self::$firestore)) {
186+
self::$firestoreClient = new FirestoreClient();
187+
}
188+
189+
self::$firestoreClient
190+
->collection(self::$collectionName)
191+
->document(self::$documentName)
192+
->set($data);
193+
}
194+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
2+
/**
3+
* Copyright 2020 Google LLC.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace Google\Cloud\Samples\Functions\FirebaseFirestore\Test;
21+
22+
use PHPUnit\Framework\TestCase;
23+
use Google\CloudFunctions\CloudEvent;
24+
use Google\Cloud\TestUtils\CloudFunctionLocalTestTrait;
25+
26+
/**
27+
* Class IntegrationTest.
28+
*
29+
* Integration Test for firebaseRTDB.
30+
*/
31+
class IntegrationTest extends TestCase
32+
{
33+
use CloudFunctionLocalTestTrait;
34+
35+
/** @var string */
36+
private static $entryPoint = 'firebaseFirestore';
37+
38+
/** @var string */
39+
private static $functionSignatureType = 'cloudevent';
40+
41+
public function dataProvider()
42+
{
43+
return [
44+
[
45+
'cloudevent' => CloudEvent::fromArray([
46+
'id' => uniqid(),
47+
'source' => 'firebase.googleapis.com',
48+
'specversion' => '1.0',
49+
'type' => 'google.cloud.firestore.document.v1.created',
50+
'data' => [
51+
'resource' => 'projects/_/instances/my-instance/refs/messages',
52+
'oldValue' => array('old' => 'value'),
53+
'value' => array('new' => 'value'),
54+
],
55+
]),
56+
'statusCode' => '200',
57+
],
58+
];
59+
}
60+
61+
/**
62+
* @dataProvider dataProvider
63+
*/
64+
public function testFirebaseFirestore(
65+
CloudEvent $cloudevent,
66+
string $statusCode
67+
): void {
68+
// Send an HTTP request using CloudEvent.
69+
$resp = $this->request($cloudevent);
70+
71+
// The Cloud Function logs all data to stderr.
72+
$actual = self::$localhost->getIncrementalErrorOutput();
73+
74+
// Confirm the status code.
75+
$this->assertEquals($statusCode, $resp->getStatusCode());
76+
77+
// Verify the data properties are logged by the function.
78+
foreach ($cloudevent->getData() as $property => $value) {
79+
if (is_string($value)) {
80+
$this->assertContains($value, $actual);
81+
}
82+
}
83+
$this->assertContains($cloudevent->getId(), $actual);
84+
}
85+
}

0 commit comments

Comments
 (0)