Skip to content

Commit 47733e1

Browse files
author
Ace Nassri
authored
feature(functions): add Firebase remote config sample (GoogleCloudPlatform#1241)
1 parent 3128b24 commit 47733e1

File tree

5 files changed

+375
-0
lines changed

5 files changed

+375
-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+
},
5+
"scripts": {
6+
"start": [
7+
"Composer\\Config::disableProcessTimeout",
8+
"FUNCTION_SIGNATURE_TYPE=cloudevent FUNCTION_TARGET=firebaseRemoteConfig php -S localhost:${PORT:-8080} vendor/bin/router.php"
9+
]
10+
},
11+
"require-dev": {
12+
"google/auth": "^1.14",
13+
"google/cloud-logging": "^1.21"
14+
}
15+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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_remote_config]
19+
20+
use Google\CloudFunctions\CloudEvent;
21+
22+
function firebaseRemoteConfig(CloudEvent $cloudevent)
23+
{
24+
$log = fopen(getenv('LOGGER_OUTPUT') ?: 'php://stderr', 'wb');
25+
26+
$data = $cloudevent->getData();
27+
28+
fwrite($log, 'Update type: ' . $data['updateType'] . PHP_EOL);
29+
fwrite($log, 'Origin: ' . $data['updateOrigin'] . PHP_EOL);
30+
fwrite($log, 'Version: ' . $data['versionNumber'] . PHP_EOL);
31+
}
32+
// [END functions_firebase_remote_config]
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 Remote Config Test Suite">
20+
<directory>testdirectory>
21+
testsuite>
22+
testsuites>
23+
<logging>
24+
<log type="coverage-clover" target="build/logs/clover.xml"/>
25+
logging>
26+
<filter>
27+
<whitelist>
28+
<directory suffix=".php">.directory>
29+
<exclude>
30+
<directory>./vendordirectory>
31+
exclude>
32+
whitelist>
33+
filter>
34+
phpunit>
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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\FirebaseRemoteConfig\Test;
21+
22+
use Google\Auth\ApplicationDefaultCredentials;
23+
use Google\Auth\CredentialsLoader;
24+
use Google\Cloud\Logging\LoggingClient;
25+
use Google\Cloud\TestUtils\CloudFunctionDeploymentTrait;
26+
use Google\Cloud\TestUtils\EventuallyConsistentTestTrait;
27+
use Google\Cloud\TestUtils\GcloudWrapper\CloudFunction;
28+
use PHPUnit\Framework\TestCase;
29+
use PHPUnit\Framework\ExpectationFailedException;
30+
31+
/**
32+
* Class DeployTest.
33+
*
34+
* This test is not run by the CI system.
35+
*
36+
* To skip deployment of a new function, run with "GOOGLE_SKIP_DEPLOYMENT=true".
37+
* To skip deletion of the tested function, run with "GOOGLE_KEEP_DEPLOYMENT=true".
38+
*/
39+
class DeployTest extends TestCase
40+
{
41+
use CloudFunctionDeploymentTrait;
42+
use EventuallyConsistentTestTrait;
43+
44+
/** @var string */
45+
private static $entryPoint = 'firebaseRemoteConfig';
46+
47+
/** @var string */
48+
private static $functionSignatureType = 'cloudevent';
49+
50+
/** @var LoggingClient */
51+
private static $loggingClient;
52+
53+
/** @var \GuzzleHttp\Client */
54+
private static $apiHttpClient;
55+
56+
/**
57+
* Deploy the Cloud Function, called from DeploymentTrait::deployApp().
58+
*
59+
* Overrides CloudFunctionDeploymentTrait::doDeploy().
60+
*/
61+
private static function doDeploy()
62+
{
63+
$project = self::requireEnv(
64+
'GOOGLE_PROJECT_ID'
65+
);
66+
67+
$event = 'google.firebase.remoteconfig.update';
68+
69+
return self::$fn->deploy([
70+
'--trigger-event' => $event
71+
], '');
72+
}
73+
74+
public function dataProvider()
75+
{
76+
$value = uniqid();
77+
return [
78+
[
79+
'label' => 'Shows update type',
80+
'key' => 'php_test',
81+
'value' => $value,
82+
'expected' => 'Update type: FORCED_UPDATE',
83+
],
84+
[
85+
'label' => 'Shows update origin',
86+
'key' => 'php_test',
87+
'value' => $value,
88+
'expected' => 'Origin: REST_API',
89+
],
90+
];
91+
}
92+
93+
/**
94+
* @dataProvider dataProvider
95+
*/
96+
public function testFirebaseRemoteConfig(
97+
string $label,
98+
string $key,
99+
string $value,
100+
string $expected
101+
): void {
102+
// Trigger config update.
103+
$objectUri = $this->updateRemoteConfig(
104+
$key,
105+
$value
106+
);
107+
108+
// Give event and log systems a head start.
109+
// If log retrieval fails to find logs for our function within retry limit, increase sleep time.
110+
sleep(5);
111+
112+
$fiveMinAgo = date(\DateTime::RFC3339, strtotime('-5 minutes'));
113+
$this->processFunctionLogs(self::$fn, $fiveMinAgo, function (\Iterator $logs) use ($expected, $label) {
114+
// Concatenate all relevant log messages.
115+
$actual = '';
116+
foreach ($logs as $log) {
117+
$info = $log->info();
118+
if (isset($info['textPayload'])) {
119+
$actual .= $info['textPayload'];
120+
}
121+
}
122+
123+
// Only testing one property to decrease odds the expected logs are
124+
// split between log requests.
125+
$this->assertContains($expected, $actual, $label);
126+
});
127+
}
128+
129+
/**
130+
* Retrieve and process logs for the defined function.
131+
*
132+
* @param CloudFunction $fn function whose logs should be checked.
133+
* @param string $startTime RFC3339 timestamp marking start of time range to retrieve.
134+
* @param callable $process callback function to run on the logs.
135+
*/
136+
private function processFunctionLogs(CloudFunction $fn, string $startTime, callable $process)
137+
{
138+
$projectId = self::requireEnv('GOOGLE_PROJECT_ID');
139+
140+
if (empty(self::$loggingClient)) {
141+
self::$loggingClient = new LoggingClient([
142+
'projectId' => $projectId
143+
]);
144+
}
145+
146+
// Define the log search criteria.
147+
$logFullName = 'projects/' . $projectId . '/logs/cloudfunctions.googleapis.com%2Fcloud-functions';
148+
$filter = sprintf(
149+
'logName="%s" resource.labels.function_name="%s" timestamp>="%s"',
150+
$logFullName,
151+
$fn->getFunctionName(),
152+
$startTime
153+
);
154+
155+
echo "\nRetrieving logs [$filter]...\n";
156+
157+
// Check for new logs for the function.
158+
$attempt = 1;
159+
$this->runEventuallyConsistentTest(function () use ($filter, $process, &$attempt) {
160+
$entries = self::$loggingClient->entries(['filter' => $filter]);
161+
162+
// If no logs came in try again.
163+
if (empty($entries->current())) {
164+
echo 'Logs not found, attempting retry #' . $attempt++ . PHP_EOL;
165+
throw new ExpectationFailedException('Log Entries not available');
166+
}
167+
echo 'Processing logs...' . PHP_EOL;
168+
169+
$process($entries);
170+
}, $retries = 10);
171+
}
172+
173+
/**
174+
* Update a value in Firebase Remote Config.
175+
*
176+
* @param string $key The key to update.
177+
* @param string $value The value to set the key to.
178+
*
179+
* @throws \RuntimeException
180+
*/
181+
private function updateRemoteConfig(
182+
string $key,
183+
string $value
184+
): void {
185+
$projectId = self::requireEnv('GOOGLE_PROJECT_ID');
186+
187+
if (empty(self::$apiHttpClient)) {
188+
$credentials = ApplicationDefaultCredentials::getCredentials('https://www.googleapis.com/auth/cloud-platform');
189+
self::$apiHttpClient = CredentialsLoader::makeHttpClient($credentials, [
190+
'base_uri' => 'https://firebaseremoteconfig.googleapis.com/v1/projects/' . $projectId . '/remoteConfig'
191+
]);
192+
}
193+
194+
$json = [
195+
'parameters' => [
196+
$key => [
197+
'defaultValue' => [
198+
'value' => $value
199+
]
200+
]
201+
]
202+
];
203+
$response = self::$apiHttpClient->put('', [
204+
'headers' => ['If-Match' => '*'],
205+
'json' => $json
206+
]);
207+
}
208+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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\FirebaseRemoteConfig\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 firebaseRemoteConfig.
30+
*/
31+
class IntegrationTest extends TestCase
32+
{
33+
use CloudFunctionLocalTestTrait;
34+
35+
/** @var string */
36+
private static $entryPoint = 'firebaseRemoteConfig';
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.firebase.remoteconfig.v1.updated',
50+
'data' => [
51+
'updateType' => 'INCREMENTAL_UPDATE',
52+
'updateOrigin' => 'CONSOLE',
53+
'versionNumber' => 2,
54+
],
55+
]),
56+
'statusCode' => '200',
57+
],
58+
];
59+
}
60+
61+
/**
62+
* @dataProvider dataProvider
63+
*/
64+
public function testFirebaseRemoteConfig(
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+
} else {
82+
$this->assertEquals($value, 2);
83+
}
84+
}
85+
}
86+
}

0 commit comments

Comments
 (0)