Skip to content

Commit 6acd713

Browse files
authored
feat: Add Authenticated Datafile Support (#200)
1 parent 4ca8bcb commit 6acd713

File tree

7 files changed

+186
-14
lines changed

7 files changed

+186
-14
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# Optimizely PHP SDK Changelog
2+
3+
## [Unreleased]
4+
5+
### New Features:
6+
- Added support for authenticated datafiles. `HTTPProjectConfigManager` now accepts `datafileAccessToken` to be able to fetch authenticated datafiles.
7+
18
## 3.3.1
29
May 28th, 2020
310

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ Create the Optimizely client, for example:
3232
$optimizely = new Optimizely(<<DATAFILE>>);
3333
```
3434

35-
Or you may also use OptimizelyFactory method to create an optimizely client using your SDK key and an optional fallback datafile. Using this method internally creates an HTTPProjectConfigManager. See [HTTPProjectConfigManager](#http_config_manager) for further detail.
35+
Or you may also use OptimizelyFactory method to create an optimizely client using your SDK key, an optional fallback datafile and an optional datafile access token. Using this method internally creates an HTTPProjectConfigManager. See [HTTPProjectConfigManager](#http_config_manager) for further detail.
3636

3737
```php
38-
$optimizelyClient = OptimizelyFactory::createDefaultInstance("your-sdk-key", <<DATAFILE>>);
38+
$optimizelyClient = OptimizelyFactory::createDefaultInstance("your-sdk-key", <<DATAFILE>>, <<DATAFILE_AUTH_TOKEN>>);
3939
```
4040
To access your HTTPProjectConfigManager:
4141

src/Optimizely/Enums/ProjectConfigManagerConstants.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22
/**
3-
* Copyright 2019, Optimizely
3+
* Copyright 2019-2020, Optimizely
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -27,7 +27,12 @@ class ProjectConfigManagerConstants
2727
/**
2828
* @const String Default URL Template to use if only SDK key is provided.
2929
*/
30-
const DEFAULT_URL_TEMPLATE = "https://cdn.optimizely.com/datafiles/%s.json";
30+
const DEFAULT_DATAFILE_URL_TEMPLATE = "https://cdn.optimizely.com/datafiles/%s.json";
31+
32+
/**
33+
* @const String Default URL Template to use if Access token is provided along with the SDK key.
34+
*/
35+
const AUTHENTICATED_DATAFILE_URL_TEMPLATE = "https://config.optimizely.com/datafiles/auth/%s.json";
3136

3237
/**
3338
* @const String to use while fetching the datafile.

src/Optimizely/OptimizelyFactory.php

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
namespace Optimizely;
1818

1919
use Optimizely\Optimizely;
20+
use Optimizely\ProjectConfigManager\HTTPProjectConfigManager;
2021

2122
/**
2223
* Class OptimizelyFactory
@@ -25,18 +26,34 @@
2526
*/
2627
class OptimizelyFactory
2728
{
28-
public static function createDefaultInstance($sdkKey, $fallbackDatafile = null)
29-
{
29+
public static function createDefaultInstance(
30+
$sdkKey,
31+
$datafile = null,
32+
$datafileAccessToken = null
33+
) {
34+
$configManager = new HTTPProjectConfigManager(
35+
$sdkKey,
36+
null,
37+
null,
38+
true,
39+
$datafile,
40+
false,
41+
null,
42+
null,
43+
null,
44+
$datafileAccessToken
45+
);
46+
3047
return new Optimizely(
31-
$fallbackDatafile,
3248
null,
3349
null,
3450
null,
3551
null,
3652
null,
3753
null,
54+
$configManager,
3855
null,
39-
$sdkKey
56+
null
4057
);
4158
}
4259
}

src/Optimizely/ProjectConfigManager/HTTPProjectConfigManager.php

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22
/**
3-
* Copyright 2019, Optimizely Inc and Contributors
3+
* Copyright 2019-2020, Optimizely Inc and Contributors
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -72,6 +72,11 @@ class HTTPProjectConfigManager implements ProjectConfigManagerInterface
7272
*/
7373
private $_notificationCenter;
7474

75+
/**
76+
* @var String datafile access token.
77+
*/
78+
private $datafileAccessToken;
79+
7580
public function __construct(
7681
$sdkKey = null,
7782
$url = null,
@@ -81,12 +86,16 @@ public function __construct(
8186
$skipJsonValidation = false,
8287
LoggerInterface $logger = null,
8388
ErrorHandlerInterface $errorHandler = null,
84-
NotificationCenter $notificationCenter = null
89+
NotificationCenter $notificationCenter = null,
90+
$datafileAccessToken = null
8591
) {
8692
$this->_skipJsonValidation = $skipJsonValidation;
8793
$this->_logger = $logger ?: new NoOpLogger();
8894
$this->_errorHandler = $errorHandler ?: new NoOpErrorHandler();
8995
$this->_notificationCenter = $notificationCenter ?: new NotificationCenter($this->_logger, $this->_errorHandler);
96+
$this->datafileAccessToken = $datafileAccessToken;
97+
$this->isDatafileAccessTokenValid = Validator::validateNonEmptyString($this->datafileAccessToken);
98+
9099
$this->httpClient = new HttpClient();
91100
$this->_url = $this->getUrl($sdkKey, $url, $urlTemplate);
92101

@@ -127,7 +136,11 @@ protected function getUrl($sdkKey, $url, $urlTemplate)
127136
}
128137

129138
if (!Validator::validateNonEmptyString($urlTemplate)) {
130-
$urlTemplate = ProjectConfigManagerConstants::DEFAULT_URL_TEMPLATE;
139+
if ($this->isDatafileAccessTokenValid) {
140+
$urlTemplate = ProjectConfigManagerConstants::AUTHENTICATED_DATAFILE_URL_TEMPLATE;
141+
} else {
142+
$urlTemplate = ProjectConfigManagerConstants::DEFAULT_DATAFILE_URL_TEMPLATE;
143+
}
131144
}
132145

133146
$url = sprintf($urlTemplate, $sdkKey);
@@ -158,11 +171,16 @@ public function fetch()
158171
*/
159172
protected function fetchDatafile()
160173
{
161-
$headers = null;
174+
$headers = [];
162175

163176
// Add If-Modified-Since header.
164177
if (Validator::validateNonEmptyString($this->_lastModifiedSince)) {
165-
$headers = array(ProjectConfigManagerConstants::IF_MODIFIED_SINCE => $this->_lastModifiedSince);
178+
$headers[ProjectConfigManagerConstants::IF_MODIFIED_SINCE] = $this->_lastModifiedSince;
179+
}
180+
181+
// Add Authorization header if access token available.
182+
if ($this->isDatafileAccessTokenValid) {
183+
$headers['Authorization'] = "Bearer {$this->datafileAccessToken}";
166184
}
167185

168186
$options = [

tests/OptimizelyFactoryTest.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use GuzzleHttp\Client;
2121
use GuzzleHttp\Handler\MockHandler;
2222
use GuzzleHttp\HandlerStack;
23+
use GuzzleHttp\Middleware;
2324
use GuzzleHttp\Psr7\Response;
2425
use Optimizely\OptimizelyFactory;
2526
use Optimizely\ProjectConfigManager\HTTPProjectConfigManager;
@@ -57,4 +58,46 @@ public function testDefaultInstance()
5758

5859
$this->assertEquals('3', $optimizelyClient->configManager->getConfig()->getRevision());
5960
}
61+
62+
public function testDefaultInstanceWithAccessToken()
63+
{
64+
$optimizelyClient = OptimizelyFactory::createDefaultInstance(
65+
"some-sdk-key",
66+
null,
67+
"some_token"
68+
);
69+
70+
// Mock http client to return a valid datafile
71+
$mock = new MockHandler([
72+
new Response(200, [], $this->typedAudiencesDataFile)
73+
]);
74+
75+
$container = [];
76+
$history = Middleware::history($container);
77+
$handler = HandlerStack::create($mock);
78+
$handler->push($history);
79+
80+
$client = new Client(['handler' => $handler]);
81+
$httpClient = new \ReflectionProperty(HTTPProjectConfigManager::class, 'httpClient');
82+
$httpClient->setAccessible(true);
83+
$httpClient->setValue($optimizelyClient->configManager, $client);
84+
85+
/// Fetch datafile
86+
$optimizelyClient->configManager->fetch();
87+
88+
$this->assertEquals('3', $optimizelyClient->configManager->getConfig()->getRevision());
89+
90+
// assert that https call is made to mock as expected.
91+
$transaction = $container[0];
92+
$this->assertEquals(
93+
'https://config.optimizely.com/datafiles/auth/some-sdk-key.json',
94+
$transaction['request']->getUri()
95+
);
96+
97+
// assert that headers include authorization access token
98+
$this->assertEquals(
99+
'Bearer some_token',
100+
$transaction['request']->getHeaders()['Authorization'][0]
101+
);
102+
}
60103
}

tests/ProjectConfigManagerTests/HTTPProjectConfigManagerTest.php

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22
/**
3-
* Copyright 2019, Optimizely
3+
* Copyright 2019-2020, Optimizely
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
2121
use GuzzleHttp\Client;
2222
use GuzzleHttp\Handler\MockHandler;
2323
use GuzzleHttp\HandlerStack;
24+
use GuzzleHttp\Middleware;
2425
use GuzzleHttp\Psr7\Response;
2526
use Monolog\Logger;
2627
use Optimizely\Config\DatafileProjectConfig;
@@ -329,6 +330,24 @@ public function testGetUrlReturnsURLWhenSdkKeyAndTemplateAreNonEmptyString()
329330
$this->assertEquals($url, 'https://custom/datafiles/sdk_key.json');
330331
}
331332

333+
public function testGetUrlReturnsURLWhenSdkKeyAndTemplateAndAccessTokenAreNonEmptyString()
334+
{
335+
$url_template = "https://custom/datafiles/%s.json";
336+
337+
$configManagerMock = $this->getMockBuilder(HTTPProjectConfigManagerTester::class)
338+
->setConstructorArgs(array('sdk_key', null, $url_template, false, DATAFILE, false,
339+
$this->loggerMock, $this->errorHandlerMock, null, 'some_token'))
340+
->setMethods(array('fetch'))
341+
->getMock();
342+
343+
$this->errorHandlerMock->expects($this->never())
344+
->method('handleError');
345+
346+
$url = $configManagerMock->getUrl('sdk_key', null, $url_template);
347+
348+
$this->assertEquals($url, 'https://custom/datafiles/sdk_key.json');
349+
}
350+
332351
public function testGetUrlReturnsURLUsingDefaultTemplateWhenTemplateIsEmptyString()
333352
{
334353
$configManagerMock = $this->getMockBuilder(HTTPProjectConfigManagerTester::class)
@@ -345,6 +364,22 @@ public function testGetUrlReturnsURLUsingDefaultTemplateWhenTemplateIsEmptyStrin
345364
$this->assertEquals($url, 'https://cdn.optimizely.com/datafiles/sdk_key.json');
346365
}
347366

367+
public function testGetUrlReturnsAuthDatafileURLWhenTemplateIsEmptyAndTokenIsProvided()
368+
{
369+
$configManagerMock = $this->getMockBuilder(HTTPProjectConfigManagerTester::class)
370+
->setConstructorArgs(array('sdk_key', null, null, false, DATAFILE, false,
371+
$this->loggerMock, $this->errorHandlerMock, null, 'some_token'))
372+
->setMethods(array('fetch'))
373+
->getMock();
374+
375+
$this->errorHandlerMock->expects($this->never())
376+
->method('handleError');
377+
378+
$url = $configManagerMock->getUrl('sdk_key', null, null);
379+
380+
$this->assertEquals($url, 'https://config.optimizely.com/datafiles/auth/sdk_key.json');
381+
}
382+
348383
public function testHandleResponseReturnsFalseForSameDatafilesRevisions()
349384
{
350385
$configManagerMock = $this->getMockBuilder(HTTPProjectConfigManagerTester::class)
@@ -367,4 +402,51 @@ public function testHandleResponseReturnsFalseForSameDatafilesRevisions()
367402
$this->assertSame($config->getRevision(), $datafile['revision']);
368403
$this->assertFalse($configManagerMock->handleResponse(DATAFILE));
369404
}
405+
406+
public function testAuthTokenInRequestHeaderWhenTokenIsProvided()
407+
{
408+
$configManager = new HTTPProjectConfigManager(
409+
'sdk_key',
410+
null,
411+
null,
412+
null,
413+
null,
414+
null,
415+
null,
416+
null,
417+
null,
418+
'access_token'
419+
);
420+
421+
// Mock http client to return a valid datafile
422+
$mock = new MockHandler([
423+
new Response(200, [], null)
424+
]);
425+
426+
$container = [];
427+
$history = Middleware::history($container);
428+
$handler = HandlerStack::create($mock);
429+
$handler->push($history);
430+
431+
$client = new Client(['handler' => $handler]);
432+
$httpClient = new \ReflectionProperty(HTTPProjectConfigManager::class, 'httpClient');
433+
$httpClient->setAccessible(true);
434+
$httpClient->setValue($configManager, $client);
435+
436+
// Fetch datafile
437+
$configManager->fetch();
438+
439+
// assert that https call is made to mock as expected.
440+
$transaction = $container[0];
441+
$this->assertEquals(
442+
'https://config.optimizely.com/datafiles/auth/sdk_key.json',
443+
$transaction['request']->getUri()
444+
);
445+
446+
// assert that headers include authorization access token
447+
$this->assertEquals(
448+
'Bearer access_token',
449+
$transaction['request']->getHeaders()['Authorization'][0]
450+
);
451+
}
370452
}

0 commit comments

Comments
 (0)