Skip to content

Commit

Permalink
feat: Tests added for the SecretKey controller (#39)
Browse files Browse the repository at this point in the history
- Refactor function names based on Laravel's best practices
- Small improve on the generateKey method
  • Loading branch information
Tmeister authored Oct 22, 2024
1 parent 79af696 commit cfda25a
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 36 deletions.
63 changes: 27 additions & 36 deletions app/Http/Controllers/API/WpOrg/SecretKey/SecretKeyController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@
namespace App\Http\Controllers\API\WpOrg\SecretKey;

use App\Http\Controllers\Controller;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Random\RandomException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class SecretKeyController extends Controller
{
// From https://github.com/wp-cli/config-command/blob/main/src/Config_Command.php
public const VALID_KEY_CHARACTERS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|';

public function index(string $version, Request $request): Response
/**
* @throws RandomException
*/
public function index(string $version): Response
{
$response = match ($version) {
'1.0' => $this->generate_1_0(),
'1.1' => $this->generate_1_1(),
'1.0' => $this->generateKey_1_0(),
'1.1' => $this->generateKey_1_1(),
default => throw new NotFoundHttpException('unsupported version'),
};

Expand All @@ -26,62 +28,51 @@ public function index(string $version, Request $request): Response

public function salt(): Response
{
return response($this->generate_salt_1_1(), 200, ['Content-Type' => 'text/plain']);
return response($this->generateSalt_1_1(), 200, ['Content-Type' => 'text/plain']);
}

private function generate_1_0(): string
/**
* @throws RandomException
*/
private function generateKey_1_0(): string
{
$key = self::unique_key();
$key = self::uniqueKey();

return "define('SECRET_KEY', '$key');\n";
// define('SECRET_KEY', '/zG$R}A5yD(&R2ob_,{g#N\\%d&MoV=NpFNFl%,e*0Zx~\"CMim^6hgQm=e20%n@er');
}

private function generate_1_1(): string
private function generateKey_1_1(): string
{
$out = '';
foreach (['AUTH_KEY', 'SECURE_AUTH_KEY', 'LOGGED_IN_KEY', 'NONCE_KEY'] as $name) {
$param = "'$name',";
$out .= sprintf("define(%-18s '%s');\n", $param, self::unique_key());
$out .= sprintf("define(%-18s '%s');\n", $param, self::uniqueKey());
}
return $out;
// define('AUTH_KEY', 'y*k]cp10`=Ut@0G9GwLD]nYX-#L_dkFyQ=7ts{,=R<T;0N/KmQ>+@Z!Up|5PQIp9');
// define('SECURE_AUTH_KEY', '{4d<t m-ktqda/*B[C{2x#;LM{rU`N-.uPMg*d-jpxk<<aW4@j{=uo|E4^dBn+zD');
// define('LOGGED_IN_KEY', '`Q7aZ[Hg9K7OYv(v:{sb6$jf]BKZ)<$)F- *8Js6Nt<IOXbV*F61-Df;@{B1%?v*');
// define('NONCE_KEY', '.rru94)V[pPQ&?Vt.qQ)wY)Wu{^cL]2*q8FDO!Z2. Y-3$HQY8s@)quO0G+gd{l$');
}

private function generate_salt_1_1(): string
private function generateSalt_1_1(): string
{
$out = '';
foreach ([
'AUTH_KEY', 'SECURE_AUTH_KEY', 'LOGGED_IN_KEY', 'NONCE_KEY',
'AUTH_SALT', 'SECURE_AUTH_SALT', 'LOGGED_IN_SALT', 'NONCE_SALT',
] as $name) {
$param = "'$name',";
$out .= sprintf("define(%-19s '%s');\n", $param, self::unique_key());
$out .= sprintf("define(%-19s '%s');\n", $param, self::uniqueKey());
}

return $out;
// define('AUTH_KEY', '1c<iaBt`UeX,.s*Au~o;4v2 +4=KQFnn#YGLs<pkK{=5Nart[]r6_*U?-y,06AJF');
// define('SECURE_AUTH_KEY', '-_bS,;!&{+Qr|z~$N6cueFO<!|Kx12!cJ7Lx-gMZ9ekSj+O2=,wRZ=2vy?r2-iLw');
// define('LOGGED_IN_KEY', '=I6gvNz[$l[|_}o[vai+.zd>zc%{cAx{4L%V-w_eLBo,S- Y !LU!QpUZ=xv<rkp');
// define('NONCE_KEY', '@{(PEa5 0q_-?dd*lb.<l5rONcT|zbohI?~8!x55W58$1KDS,LeH|{#7Z-0-_<R3');
// define('AUTH_SALT', '0@jMX6(oj>M?q-@H_.dSjEyK=}_O+ u[@I2S!lA?8v}7HrL/I.{p07U3<0Dzb|p`');
// define('SECURE_AUTH_SALT', '{sAB~6ta%o8o3|`$!8I5$`fS7M3X9VTX1*ZoX&/9_b^QID+pbds^.HYEewz^xCwB');
// define('LOGGED_IN_SALT', 'kj,?{%-qhT}#P Q?+oSjLN;^cZu=,V2ZjSHI;XgU-h@`H+?1?::muJ*4--&~!$.+');
// define('NONCE_SALT', ':nUJz*%EU1R0 3x<8`=:>PC+^Vhk9DjyjI@tjuMVOj@dk(N_jn-(+AC/I7wOs`yT');
}

private static function unique_key(int $length = 64): string
/**
* @throws RandomException
*/
private static function uniqueKey(int $length = 64): string
{
$chars = self::VALID_KEY_CHARACTERS;
$key = '';
$len = strlen($chars) - 1;

for ($i = 0; $i < $length; $i++) {
$key .= $chars[random_int(0, $len)];
}

return $key;
return implode(array_map(
static fn() => self::VALID_KEY_CHARACTERS[random_int(0, $length)],
array_fill(0, $length, null)
));
}

}
95 changes: 95 additions & 0 deletions tests/Feature/API/WpOrg/SecretKeyControllerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

namespace Tests\Feature\API\WpOrg;

use App\Http\Controllers\API\WpOrg\SecretKey\SecretKeyController;
use Exception;

$validKeys = SecretKeyController::VALID_KEY_CHARACTERS;

// Helper function to validate keys
function validateKeys(string $content, array $expectedKeyNames, string $validKeys): void
{
foreach ($expectedKeyNames as $keyName) {
preg_match("/define\('$keyName',\s+'([^']+)'\);/", $content, $matches);

// Ensure we have a match and a captured group
expect($matches)->toHaveCount(2);

// Extract the key value from the matches
$keyValue = $matches[1];

// Validate that the key contains only valid characters
expect(preg_match('/^[' . preg_quote($validKeys, '/') . ']{64}$/', $keyValue))
->toBe(1)
->and(preg_match(
'/^define\(\'' . preg_quote(
$keyName,
'/'
) . '\',\s+\'[^\']+\'\);$/',
$matches[0]
))->toBe(1);
}
}

it(
'can generate a secret keys for version 1.0 and 1.1',
function (string $version, int $expectedKeys) use ($validKeys) {
$response = $this->getJson("/secret-key/$version");

expect($response->getStatusCode())
->toBe(200)
->and($response->headers->get('Content-Type'))->toContain('text/plain');

$expectedKeyNames = match ($version) {
'1.0' => [ 'SECRET_KEY' ],
'1.1' => [ 'AUTH_KEY', 'SECURE_AUTH_KEY', 'LOGGED_IN_KEY', 'NONCE_KEY' ],
};

$content = $response->getContent();

// Validate the number of keys, the +1 is for the last line break
expect(explode("\n", $content))->toHaveCount($expectedKeys + 1);

validateKeys($content, $expectedKeyNames, $validKeys);
}
)->with([
'1.0 version' => [ '1.0', 1 ],
'1.1 version' => [ '1.1', 4 ],
]);

it('can generate a secret keys with salt for version 1.1', function () use ($validKeys) {
$response = $this->getJson('/secret-key/1.1/salt');

expect($response->getStatusCode())
->toBe(200)
->and($response->headers->get('Content-Type'))->toContain('text/plain');

$content = $response->getContent();

// Validate the number of keys, the +1 is for the last line break
expect(explode("\n", $content))->toHaveCount(8 + 1);

$expectedKeyNames = [
'AUTH_KEY',
'SECURE_AUTH_KEY',
'LOGGED_IN_KEY',
'NONCE_KEY',
'AUTH_SALT',
'SECURE_AUTH_SALT',
'LOGGED_IN_SALT',
'NONCE_SALT',
];

validateKeys($content, $expectedKeyNames, $validKeys);
});

it('returns 404 for unsupported salt versions', function () {
$response = $this->getJson('/secret-key/1.0/salt');
expect($response->getStatusCode())->toBe(404);
});

it('returns 404 for unsupported secret key versions', function () {
$response = $this->getJson('/secret-key/2.0');
expect($response->getStatusCode())->toBe(404);
});

0 comments on commit cfda25a

Please sign in to comment.