Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unit tests #8

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: "Code Coverage"

on:
push:
branches: [ "main" ,"switch-to-s2s-oauth-apps-tests"]
pull_request:
branches: [ "main" ,"switch-to-s2s-oauth-apps-tests"]

jobs:
code-coverage:

runs-on: "ubuntu-latest"

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install PHP with extensions
uses: shivammathur/setup-php@v2
with:
coverage: xdebug
php-version: '8.1'

- name: Install dependencies
run: composer update --ansi --no-interaction --no-progress --optimize-autoloader

- name: Collect code coverage with PHPUnit
run: Packages/Libraries/phpunit/phpunit/phpunit --bootstrap Tests/UnitTestBootstrap.php --configuration Tests/phpunit.xml --colors=always --coverage-clover=.build/logs/clover.xml

- name: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v3
27 changes: 27 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: PHPUnit Tests

on:
push:
branches: [ "main" ,"switch-to-s2s-oauth-apps-tests"]
pull_request:
branches: [ "main" ,"switch-to-s2s-oauth-apps-tests"]

jobs:
run-tests:

runs-on: ubuntu-latest

steps:
- name: "Checkout code"
uses: actions/checkout@v4

- name: "Install PHP with extensions"
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'

- name: Install Dependencies
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist

- name: Execute tests via PHPUnit
run: composer test
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ node_modules
*.DS_Store
/.idea
.remote-sync.json
/bin
/Packages
composer.lock
.phpunit.result.cache
Tests/Reports
19 changes: 19 additions & 0 deletions Classes/Domain/Model/ZoomApiAccessToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace CodeQ\ZoomApi\Domain\Model;

class ZoomApiAccessToken
{
public readonly string $accessToken;
public readonly array $scope;
gradinarufelix marked this conversation as resolved.
Show resolved Hide resolved

/**
* @param string $accessToken
* @param array $scope
*/
public function __construct(string $accessToken, array $scope)
{
$this->accessToken = $accessToken;
$this->scope = $scope;
}
}
93 changes: 93 additions & 0 deletions Classes/Domain/Service/ZoomApiAccessTokenFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

namespace CodeQ\ZoomApi\Domain\Service;

use CodeQ\ZoomApi\Domain\Model\ZoomApiAccessToken;
use CodeQ\ZoomApi\ZoomApiException;
use GuzzleHttp\Client;
use Neos\Flow\Annotations as Flow;
use Psr\Log\LoggerInterface;

/**
* @Flow\Scope("singleton")
*/
class ZoomApiAccessTokenFactory
{
private Client $client;

/**
* @Flow\InjectConfiguration
* @var array
*/
protected array $settings;

/**
* @Flow\Inject
* @var LoggerInterface
*/
protected $systemLogger;

/**
* @return void
* @throws ZoomApiException
*/
public function initializeObject(): void
{
$zoomApiClientId = $this->settings['auth']['clientId'];
$zoomApiClientSecret = $this->settings['auth']['clientSecret'];
if (!$zoomApiClientId || !$zoomApiClientSecret) {
throw new ZoomApiException('Please set a Zoom Account ID, Client ID and Secret for CodeQ.ZoomApi to be able to authenticate.', 1695830249149);
}
$this->client = $this->buildClient($zoomApiClientId, $zoomApiClientSecret);
}

/**
* @return ZoomApiAccessToken
* @throws ZoomApiException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function createFromConfiguration(): ZoomApiAccessToken
{
$zoomApiAccountId = $this->settings['auth']['accountId'];
if (!$zoomApiAccountId) {
throw new ZoomApiException('Please set a Zoom Account ID for CodeQ.ZoomApi to be able to authenticate.', 1695904285296);
}
$response = $this->client->post('oauth/token', [
'form_params' => [
'grant_type' => 'account_credentials',
'account_id' => $zoomApiAccountId
]
]);

if ($response->getStatusCode() !== 200) {
throw new ZoomApiException('Could not fetch Zoom access token. Please check the settings for account ID, client ID and client secret, as well as your Zoom app.', 1695040346621);
}
gradinarufelix marked this conversation as resolved.
Show resolved Hide resolved

$responseBodyAsArray = json_decode($response->getBody()->getContents(), true);

if (!str_contains($responseBodyAsArray['scope'], 'user:read:admin') || !str_contains($responseBodyAsArray['scope'], 'recording:read:admin') || !str_contains($responseBodyAsArray['scope'], 'meeting:read:admin')) {
throw new ZoomApiException('Please ensure your Zoom app has the following scopes: user:read:admin, recording:read:admin, meeting:read:admin', 1695040540417);
}

return new ZoomApiAccessToken(
$responseBodyAsArray['access_token'],
explode(',', $responseBodyAsArray['scope']));
}
gradinarufelix marked this conversation as resolved.
Show resolved Hide resolved

/**
* @param string $zoomApiClientId
* @param string $zoomApiClientSecret
*
* @return Client
*/
protected function buildClient(string $zoomApiClientId, string $zoomApiClientSecret): Client
{
return (new Client([
'base_uri' => 'https://zoom.us/',
'headers' => [
'Authorization' => "Basic " . base64_encode($zoomApiClientId . ':' . $zoomApiClientSecret),
'Content-Type' => 'application/json',
],
]));
}
}
108 changes: 43 additions & 65 deletions Classes/Domain/Service/ZoomApiService.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ class ZoomApiService
private Client $client;

/**
* @Flow\InjectConfiguration
* @var array
* @Flow\Inject
* @var ZoomApiAccessTokenFactory
*/
protected array $settings;
protected $accessTokenFactory;

/**
* @var VariableFrontend
Expand All @@ -45,16 +45,13 @@ class ZoomApiService
*/
public function initializeObject(): void
{
$zoomApiAccountId = $this->settings['auth']['accountId'];
$zoomApiClientId = $this->settings['auth']['clientId'];
$zoomApiClientSecret = $this->settings['auth']['clientSecret'];
if (!$zoomApiAccountId || !$zoomApiClientId || !$zoomApiClientSecret) {
throw new ZoomApiException('Please set a Zoom Account ID, Client ID and Secret for CodeQ.ZoomApi to be able to authenticate.');
}

$accessToken = $this->getAccessToken($zoomApiAccountId, $zoomApiClientId, $zoomApiClientSecret);
$accessToken = $this->accessTokenFactory->createFromConfiguration();
$this->client = $this->buildClient($accessToken->accessToken);
}

$this->client = (new Client([
protected function buildClient(string $accessToken): Client
{
return (new Client([
'base_uri' => 'https://api.zoom.us/v2/',
'headers' => [
'Authorization' => "Bearer $accessToken",
Expand Down Expand Up @@ -110,29 +107,14 @@ public function getUpcomingMeetings(bool $skipCache = false): array
*/
public function getRecordings(DateTime|string $from, DateTime|string $to, bool $skipCache = false): array
{
$cacheEntryIdentifier = sprintf('recordings_%s_%s', is_string($from) ? $from : $from->format('Y-m-d'), is_string($to) ? $to : $to->format('Y-m-d'));
list($from, $to) = $this->convertFromAndToAsDateTimeImmutable($from, $to);
gradinarufelix marked this conversation as resolved.
Show resolved Hide resolved

$cacheEntryIdentifier = sprintf('recordings_%s_%s', $from->format('Y-m-d'), $to->format('Y-m-d'));
$recordings = $this->getCacheEntry($cacheEntryIdentifier);
gradinarufelix marked this conversation as resolved.
Show resolved Hide resolved
if (!$skipCache && $recordings !== false) {
return $recordings;
}

if (is_string($from)) {
$from = new DateTimeImmutable($from);
} else {
$from = DateTimeImmutable::createFromMutable($from);
}

if (is_string($to)) {
$to = new DateTimeImmutable($to);
} else {
$to = DateTimeImmutable::createFromMutable($to);
}

if ($from > $to) {
throw new InvalidArgumentException('The from date must be after the to date');
}

$recordings = $this->fetchDataForDateRange($from, $to);

try {
Expand Down Expand Up @@ -236,7 +218,13 @@ private function fetchPaginatedData(string $uri, string $paginatedDataKey): arra
throw new ZoomApiException(sprintf('Could not fetch Zoom paginated data for data key "%s", returned status "%s"', $paginatedDataKey, $response->getStatusCode()), 1695239983421);
Copy link
Contributor

@krsriq krsriq Oct 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is still suspiciously not covered by tests - and has a bug (guess you can find it on your own ;) ).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sollte jetzt gefixt sein, was meinst du? ;-)

Ich habe jetzt auch noch PHPStan hinzugefügt und Codesniffer

}
$bodyContents = $response->getBody()->getContents();
return json_decode($bodyContents, true);
$bodyContentsArray = json_decode($bodyContents, true);

if ($bodyContentsArray === null || !array_key_exists($paginatedDataKey, $bodyContentsArray)) {
throw new ZoomApiException(sprintf('Could not fetch Zoom paginated data for data key "%s", returned empty response', $paginatedDataKey), 1695828849253);
}

return $bodyContentsArray;
}

private function dateDifferenceIsBiggerThanOneMonth(DateTimeImmutable $from, DateTimeImmutable $to): bool
Expand All @@ -247,49 +235,39 @@ private function dateDifferenceIsBiggerThanOneMonth(DateTimeImmutable $from, Dat
}

/**
* @param string $accountId
* @param string $zoomApiClientId
* @param string $zoomApiClientSecret
* @param string $entryIdentifier
*
* @return string|null
* @throws GuzzleException|ZoomApiException
* @return array|bool
*/
private function getAccessToken(string $accountId, string $zoomApiClientId, string $zoomApiClientSecret): ?string
private function getCacheEntry(string $entryIdentifier): array|bool
{
$client = new Client([
'base_uri' => 'https://zoom.us/',
'headers' => [
'Authorization' => "Basic " . base64_encode($zoomApiClientId . ':' . $zoomApiClientSecret),
'Content-Type' => 'application/json',
],
]);
$response = $client->post('oauth/token', [
'form_params' => [
'grant_type' => 'account_credentials',
'account_id' => $accountId
]
]);

if ($response->getStatusCode() !== 200) {
throw new ZoomApiException('Could not fetch Zoom access token. Please check the settings for account ID, client ID and client secret, as well as your Zoom app.', 1695040346621);
}

$responseBodyAsArray = json_decode($response->getBody()->getContents(), true);

if (!str_contains($responseBodyAsArray['scope'], 'user:read:admin') || !str_contains($responseBodyAsArray['scope'], 'recording:read:admin') || !str_contains($responseBodyAsArray['scope'], 'meeting:read:admin')) {
throw new ZoomApiException('Please ensure your Zoom app has the following scopes: user:read:admin, recording:read:admin, meeting:read:admin', 1695040540417);
}

return $responseBodyAsArray['access_token'];
return $this->requestsCache->get($entryIdentifier);
}

/**
* @param string $entryIdentifier
* @param DateTime|string $from
* @param DateTime|string $to
*
* @return array|bool
* @return DateTimeImmutable[]
* @throws Exception
*/
private function getCacheEntry(string $entryIdentifier): array|bool
protected function convertFromAndToAsDateTimeImmutable(DateTime|string $from, DateTime|string $to): array
{
return $this->requestsCache->get($entryIdentifier);
if (is_string($from)) {
$from = new DateTimeImmutable($from);
} else {
$from = DateTimeImmutable::createFromMutable($from);
}

if (is_string($to)) {
$to = new DateTimeImmutable($to);
} else {
$to = DateTimeImmutable::createFromMutable($to);
}

if ($from > $to) {
throw new InvalidArgumentException('The from date must be after the to date');
}
return array($from, $to);
gradinarufelix marked this conversation as resolved.
Show resolved Hide resolved
}
}
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
[![Latest Stable Version](https://poser.pugx.org/codeq/zoom-api/v/stable)](https://packagist.org/packages/codeq/zoom-api)
[![License](https://poser.pugx.org/codeq/zoom-api/license)](LICENSE)
[![Code Coverage](https://codecov.io/gh/code-q-web-factory/CodeQ.ZoomApi/branch/switch-to-s2s-oauth-apps-tests/graph/badge.svg)](https://codecov.io/gh/code-q-web-factory/CodeQ.ZoomApi)


# CodeQ.ZoomApi

Expand Down Expand Up @@ -49,3 +51,11 @@ CodeQ_ZoomApi_Requests:
```

Of course, you can also switch to a different cache backend at your convenience.


## Testing

To run the unit tests, execute the following commands:

1. `composer install`
2. `composer test`
Loading