Major Code Review: Happy New Year 2023 (#877)

pull/879/head
Kei 1 year ago committed by GitHub
parent bb85b3435b
commit b59ef9a77e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -4,7 +4,6 @@ APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
UH_ANONYMIZE_IP_ADDR=true
UH_PUBLIC_SITE=true
UH_REGISTRATION=true
UH_HASH_LENGTH=6

@ -4,7 +4,6 @@ APP_KEY=base64:SsuVzwH9xQnlEqSgTdTbBKyJ0rYrgt5Wr71vv02jDV8=
APP_DEBUG=true
APP_URL=http://localhost
UH_ANONYMIZE_IP_ADDR=true
UH_PUBLIC_SITE=true
UH_REGISTRATION=true
UH_HASH_LENGTH=6

@ -1,65 +0,0 @@
<?php
namespace App\Actions;
use App\Helpers\Helper;
use App\Models\Url;
use App\Models\Visit;
class UrlRedirectAction
{
public function __construct(
public Visit $visit,
) {
}
/**
* Handle the HTTP redirect and return the redirect response.
*
* Redirect client to an existing short URL (no check performed) and
* execute tasks update clicks for short URL.
*
* @param Url $url \App\Models\Url
* @return \Illuminate\Http\RedirectResponse
*/
public function handleHttpRedirect(Url $url)
{
$this->storeVisitStat($url);
$statusCode = (int) config('urlhub.redirect_status_code');
$maxAge = (int) config('urlhub.redirect_cache_max_age');
$headers = [
'Cache-Control' => sprintf('private,max-age=%s', $maxAge),
];
return redirect()->away($url->destination, $statusCode, $headers);
}
/**
* Create visit statistics and store it in the database.
*
* @param Url $url \App\Models\Url
* @return void
*/
private function storeVisitStat(Url $url)
{
$logBotVisit = config('urlhub.track_bot_visits');
if ($logBotVisit === false && \Browser::isBot() === true) {
return;
}
Visit::create([
'url_id' => $url->id,
'url_author_id' => $url->user->id,
'visitor_id' => $this->visit->visitorId(),
'is_first_click' => $this->visit->isFirstClick($url),
'referer' => request()->header('referer'),
'ip' => Helper::anonymizeIp(request()->ip()),
'browser' => \Browser::browserFamily(),
'browser_version' => \Browser::browserVersion(),
'device' => \Browser::deviceType(),
'os' => \Browser::platformFamily(),
'os_version' => \Browser::platformVersion(),
]);
}
}

@ -1,16 +0,0 @@
<?php
use App\Helpers\NumHelper;
if (! function_exists('compactNumber')) {
/**
* \App\Helpers\NumHelper::compactNumber()
*
* @param int $value
* @return int|string
*/
function compactNumber($value)
{
return NumHelper::number_shorten($value);
}
}

@ -5,24 +5,9 @@ namespace App\Helpers;
use Illuminate\Support\Str;
use Illuminate\Support\Stringable;
use Spatie\Url\Url as SpatieUrl;
use Symfony\Component\HttpFoundation\IpUtils;
class Helper
{
/**
* Anonymize an IPv4 or IPv6 address.
*
* @param string|null $address
*/
public static function anonymizeIp($address): string
{
if (config('urlhub.anonymize_ip_addr') === false) {
return $address;
}
return IPUtils::anonymize($address);
}
/**
* Display the link according to what You need.
*

@ -1,63 +0,0 @@
<?php
namespace App\Helpers;
class NumHelper
{
/**
* Convert large positive numbers in to short form like 1K+, 100K+, 199K+, 1M+, 10M+,
* 1B+ etc.
* Based on: ({@link https://gist.github.com/RadGH/84edff0cc81e6326029c}).
*/
public static function number_shorten(int $number): int|string
{
$nFormat = floor($number);
$suffix = '';
if ($number >= pow(10, 3) && $number < pow(10, 6)) {
// 1k-999k
$nFormat = self::numbPrec($number / pow(10, 3));
$suffix = 'K+';
if (($number / pow(10, 3) === 1) || ($number / pow(10, 4) === 1) || ($number / pow(10, 5) === 1)) {
$suffix = 'K';
}
} elseif ($number >= pow(10, 6) && $number < pow(10, 9)) {
// 1m-999m
$nFormat = self::numbPrec($number / pow(10, 6));
$suffix = 'M+';
if (($number / pow(10, 6) === 1) || ($number / pow(10, 7) === 1) || ($number / pow(10, 8) === 1)) {
$suffix = 'M';
}
} elseif ($number >= pow(10, 9) && $number < pow(10, 12)) {
// 1b-999b
$nFormat = self::numbPrec($number / pow(10, 9));
$suffix = 'B+';
if (($number / pow(10, 9) === 1) || ($number / pow(10, 10) === 1) || ($number / pow(10, 11) === 1)) {
$suffix = 'B';
}
} elseif ($number >= pow(10, 12)) {
// 1t+
$nFormat = self::numbPrec($number / pow(10, 12));
$suffix = 'T+';
if (($number / pow(10, 12) === 1) || ($number / pow(10, 13) === 1) || ($number / pow(10, 14) === 1)) {
$suffix = 'T';
}
}
return ! empty($nFormat.$suffix) ? $nFormat.$suffix : 0;
}
/**
* Alternative to make number_format() not to round numbers up.
*
* Based on: (@see https://stackoverflow.com/q/3833137).
*/
public static function numbPrec(float $number, int $precision = 2): float
{
return floor($number * pow(10, $precision)) / pow(10, $precision);
}
}

@ -1,43 +0,0 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreUrl;
use App\Models\Url;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Validator;
class UrlController extends Controller
{
/**
* UrlController constructor.
*/
public function __construct(
public Url $url
) {
$this->middleware('urlhublinkchecker')->only('create');
}
/**
* Store the data the user sent to create the Short URL.
*
* @param StoreUrl $request \App\Http\Requests\StoreUrl
* @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response
*/
public function store(StoreUrl $request)
{
$v = Validator::make($request->all(), (new StoreUrl)->rules());
if ($v->fails()) {
return response()->json(['errors' => $v->errors()->all()]);
}
$url = $this->url->shortenUrl($request, auth()->id());
return response([
'id' => $url->id,
'long_url' => $url->destination,
'short_url' => url($url->keyword),
], Response::HTTP_CREATED);
}
}

@ -3,6 +3,7 @@
namespace App\Http\Controllers\Dashboard;
use App\Http\Controllers\Controller;
use App\Models\Url;
class AllUrlController extends Controller
{
@ -27,10 +28,10 @@ class AllUrlController extends Controller
/**
* Delete a Short URL on user (Admin) request.
*
* @param mixed $url
* @param Url $url \App\Models\Url
* @return \Illuminate\Http\RedirectResponse
*/
public function delete($url)
public function delete(Url $url)
{
$url->delete();

@ -5,7 +5,8 @@ namespace App\Http\Controllers\Dashboard;
use App\Http\Controllers\Controller;
use App\Models\Url;
use App\Models\User;
use App\Models\Visit;
use App\Services\KeyGeneratorService;
use App\Services\UHubLinkService;
use Illuminate\Http\Request;
class DashboardController extends Controller
@ -13,7 +14,7 @@ class DashboardController extends Controller
public function __construct(
public Url $url,
public User $user,
public Visit $visit
public UHubLinkService $uHubLinkService,
) {
}
@ -27,39 +28,37 @@ class DashboardController extends Controller
return view('backend.dashboard', [
'url' => $this->url,
'user' => $this->user,
'visit' => $this->visit,
'keyGeneratorService' => app(KeyGeneratorService::class),
]);
}
/**
* Show shortened url details page
*
* @param mixed $key
* @param string $urlKey A unique key for the shortened URL
* @return \Illuminate\Contracts\View\View
*/
public function edit($key)
public function edit(string $urlKey)
{
$url = Url::whereKeyword($key)->firstOrFail();
$url = Url::whereKeyword($urlKey)->firstOrFail();
$this->authorize('updateUrl', $url);
return view('backend.edit', compact('url'));
return view('backend.edit', ['url' => $url]);
}
/**
* Update the destination URL
*
* @param Request $request \Illuminate\Http\Request
* @param mixed $url
* @param Url $url \App\Models\Url
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(Request $request, $url)
public function update(Request $request, Url $url)
{
$url->destination = $request->long_url;
$url->title = $request->title;
$url->save();
$this->uHubLinkService->update($request, $url);
return to_route('dashboard')
->withFlashSuccess(__('Link changed successfully !'));
@ -68,12 +67,12 @@ class DashboardController extends Controller
/**
* Delete shortened URLs
*
* @param mixed $url
* @param Url $url \App\Models\Url
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function delete($url)
public function delete(Url $url)
{
$this->authorize('forceDelete', $url);
@ -84,12 +83,12 @@ class DashboardController extends Controller
}
/**
* @param mixed $key
* @param string $urlKey A unique key for the shortened URL
* @return \Illuminate\Http\RedirectResponse
*/
public function duplicate($key)
public function duplicate($urlKey)
{
$this->url->duplicate($key, auth()->id());
$this->uHubLinkService->duplicate($urlKey);
return redirect()->back()
->withFlashSuccess(__('The link has successfully duplicated.'));

@ -21,7 +21,7 @@ class ChangePasswordController extends Controller
{
$this->authorize('view', $user);
return view('backend.user.changepassword', compact('user'));
return view('backend.user.changepassword', ['user' => $user]);
}
/**

@ -38,7 +38,7 @@ class UserController extends Controller
{
$this->authorize('view', $user);
return view('backend.user.profile', compact('user'));
return view('backend.user.profile', ['user' => $user]);
}
/**

@ -2,9 +2,10 @@
namespace App\Http\Controllers;
use App\Actions\QrCodeAction;
use App\Http\Requests\StoreUrl;
use App\Models\Url;
use App\Services\QrCodeService;
use App\Services\UHubLinkService;
class UrlController extends Controller
{
@ -12,7 +13,8 @@ class UrlController extends Controller
* UrlController constructor.
*/
public function __construct(
public Url $url
public Url $url,
public UHubLinkService $uHubLinkService,
) {
$this->middleware('urlhublinkchecker')->only('create');
}
@ -25,7 +27,7 @@ class UrlController extends Controller
*/
public function create(StoreUrl $request)
{
$url = $this->url->shortenUrl($request, auth()->id());
$url = $this->uHubLinkService->create($request);
return to_route('su_detail', $url->keyword);
}
@ -35,18 +37,21 @@ class UrlController extends Controller
*
* @codeCoverageIgnore
*
* @param string $key
* @param string $urlKey A unique key for the shortened URL
* @return \Illuminate\Contracts\View\View
*/
public function showDetail($key)
public function showDetail(string $urlKey)
{
$url = Url::with('visit')->whereKeyword($key)->firstOrFail();
$data = ['url' => $url, 'visit' => new \App\Models\Visit];
$url = Url::with('visit')->whereKeyword($urlKey)->firstOrFail();
$data = [
'url' => $url,
'visit' => new \App\Models\Visit,
];
if (config('urlhub.qrcode')) {
$qrCode = (new QrCodeAction)->process($url->short_url);
$qrCode = app(QrCodeService::class)->execute($url->short_url);
$data = array_merge($data, compact(['qrCode']));
$data = array_merge($data, ['qrCode' => $qrCode]);
}
return view('frontend.short', $data);
@ -55,12 +60,12 @@ class UrlController extends Controller
/**
* Delete a shortened URL on user request.
*
* @param mixed $url
* @param Url $url \App\Models\Url
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function delete($url)
public function delete(Url $url)
{
$this->authorize('forceDelete', $url);
@ -74,14 +79,14 @@ class UrlController extends Controller
* link. You can duplicate it and it will generated a new unique random
* key.
*
* @param string $urlKey A unique key for the shortened URL
* @return \Illuminate\Http\RedirectResponse
*/
public function duplicate(string $key)
public function duplicate(string $urlKey)
{
$randomKey = $this->url->randomString();
$this->url->duplicate($key, auth()->id(), $randomKey);
$this->uHubLinkService->duplicate($urlKey);
return to_route('su_detail', $randomKey)
return to_route('su_detail', $this->uHubLinkService->new_keyword)
->withFlashSuccess(__('The link has successfully duplicated.'));
}
}

@ -2,24 +2,38 @@
namespace App\Http\Controllers;
use App\Actions\UrlRedirectAction;
use App\Models\Url;
use App\Services\UrlRedirection;
use App\Services\VisitorService;
use Illuminate\Support\Facades\DB;
class UrlRedirectController extends Controller
{
public function __construct(
public UrlRedirection $urlRedirection,
public VisitorService $visitorService,
) {
}
/**
* Handle the logging of the URL and redirect the user to the intended
* long URL.
* Redirect the client to the intended long URL (no checks are performed)
* and executes the create visitor data task.
*
* @param string $urlKey A unique key for the shortened URL
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException<\App\Models\Url>
*/
public function __invoke(UrlRedirectAction $action, string $key)
public function __invoke(string $urlKey)
{
return DB::transaction(function () use ($action, $key) {
$url = Url::whereKeyword($key)->firstOrFail();
return DB::transaction(function () use ($urlKey) {
// firstOrFail() will throw a ModelNotFoundException if the URL is not
// found and 404 will be returned to the client.
$url = Url::whereKeyword($urlKey)->firstOrFail();
$this->visitorService->create($url);
return $action->handleHttpRedirect($url);
return $this->urlRedirection->execute($url);
});
}
}

@ -4,7 +4,6 @@ namespace App\Http\Livewire\Table;
use App\Helpers\Helper;
use App\Models\Url;
use App\Models\Visit;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Str;
@ -106,10 +105,9 @@ final class AllUlrTable extends PowerGridComponent
.Blade::render('@svg(\'icon-open-in-new\', \'!h-[0.7em] ml-1\')').
'</a>';
})
->addColumn('click', function (Url $url) {
$visit = new Visit;
$uClick = Helper::compactNumber($visit->totalClickPerUrl($url->id, unique: true));
$tClick = Helper::compactNumber($visit->totalClickPerUrl($url->id));
->addColumn('t_clicks', function (Url $url) {
$uClick = Helper::compactNumber($url->uniqueClicks);
$tClick = Helper::compactNumber($url->clicks);
$icon = Blade::render('@svg(\'icon-bar-chart\', \'ml-2 text-indigo-600\')');
$title = $uClick.' '.__('Uniques').' / '.$tClick.' '.__('Clicks');
@ -186,7 +184,7 @@ final class AllUlrTable extends PowerGridComponent
Column::add()
->title('CLICKS')
->field('click'),
->field('t_clicks'),
Column::add()
->title('CREATED AT')

@ -4,7 +4,6 @@ namespace App\Http\Livewire\Table;
use App\Helpers\Helper;
use App\Models\Url;
use App\Models\Visit;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Str;
@ -101,10 +100,9 @@ final class MyUrlTable extends PowerGridComponent
.Blade::render('@svg(\'icon-open-in-new\', \'!h-[0.7em] ml-1\')').
'</a>';
})
->addColumn('click', function (Url $url) {
$visit = new Visit;
$uClick = Helper::compactNumber($visit->totalClickPerUrl($url->id, unique: true));
$tClick = Helper::compactNumber($visit->totalClickPerUrl($url->id));
->addColumn('t_clicks', function (Url $url) {
$uClick = Helper::compactNumber($url->uniqueClicks);
$tClick = Helper::compactNumber($url->clicks);
$icon = Blade::render('@svg(\'icon-bar-chart\', \'ml-2 text-indigo-600\')');
$title = $uClick.' '.__('Uniques').' / '.$tClick.' '.__('Clicks');
@ -175,7 +173,7 @@ final class MyUrlTable extends PowerGridComponent
Column::add()
->title('CLICKS')
->field('click'),
->field('t_clicks'),
Column::add()
->title('CREATED AT')

@ -3,22 +3,18 @@
namespace App\Http\Middleware;
use App\Models\Url;
use App\Services\KeyGeneratorService;
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
class UrlHubLinkChecker
{
public function __construct(
public Url $url
) {
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @return mixed
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle($request, \Closure $next)
public function handle(Request $request, \Closure $next)
{
if ($this->customKeywordIsAcceptable($request) === false) {
return redirect()->back()
@ -47,10 +43,8 @@ class UrlHubLinkChecker
*
* - Prevent registered routes from being used as custom keywords.
* - Prevent using blacklisted words or reserved keywords as custom keywords.
*
* @param \Illuminate\Http\Request $request
*/
private function customKeywordIsAcceptable($request): bool
private function customKeywordIsAcceptable(Request $request): bool
{
$value = $request->custom_key;
$routes = array_map(
@ -74,7 +68,7 @@ class UrlHubLinkChecker
*/
private function canGenerateUniqueRandomKeys(): bool
{
if ($this->url->keyRemaining() === 0) {
if (app(KeyGeneratorService::class)->idleCapacity() === 0) {
return false;
}
@ -83,10 +77,8 @@ class UrlHubLinkChecker
/**
* Check if a destination URL already exists in the database.
*
* @param \Illuminate\Http\Request $request
*/
private function destinationUrlAlreadyExists($request): Url|null
private function destinationUrlAlreadyExists(Request $request): Url|null
{
$longUrl = rtrim($request->long_url, '/'); // Remove trailing slash

@ -21,7 +21,7 @@ class StoreUrl extends FormRequest
/**
* Get the validation rules that apply to the request.
*
* @return array
* @return array<string, mixed>
*/
public function rules()
{
@ -34,7 +34,7 @@ class StoreUrl extends FormRequest
/**
* Get the error messages for the defined validation rules.
*
* @return array
* @return array<string, mixed>
*/
public function messages()
{

@ -19,7 +19,7 @@ class UpdateUserEmail extends FormRequest
/**
* Get the validation rules that apply to the request.
*
* @return array
* @return array<string, mixed>
*/
public function rules()
{

@ -20,7 +20,7 @@ class UpdateUserPassword extends FormRequest
/**
* Get the validation rules that apply to the request.
*
* @return array
* @return array<string, mixed>
*/
public function rules()
{

@ -2,21 +2,17 @@
namespace App\Models;
use App\Helpers\Helper;
use App\Http\Requests\StoreUrl;
use App\Models\Traits\Hashidable;
use Embed\Embed;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Spatie\Url\Url as SpatieUrl;
/**
* @property int|null $user_id
* @property string $short_url
* @property string $destination
* @property string $title
* @property int $clicks
* @property int $uniqueClicks
*/
class Url extends Model
{
@ -89,7 +85,7 @@ class Url extends Model
protected function shortUrl(): Attribute
{
return Attribute::make(
get: fn ($value, $attributes) => url('/'.$attributes['keyword']),
get: fn ($value, $attr) => url('/'.$attr['keyword']),
);
}
@ -100,20 +96,17 @@ class Url extends Model
);
}
protected function title(): Attribute
protected function clicks(): Attribute
{
return Attribute::make(
set: function ($value) {
if (config('urlhub.web_title')) {
if (Str::startsWith($value, 'http')) {
return $this->getWebTitle($value);
}
return $value;
}
get: fn ($value, $attr) => $this->numberOfClicks($attr['id']),
);
}
return 'No Title';
},
protected function uniqueClicks(): Attribute
{
return Attribute::make(
get: fn ($value, $attr) => $this->numberOfClicks($attr['id'], unique: true),
);
}
@ -124,214 +117,70 @@ class Url extends Model
*/
/**
* @param StoreUrl $request \App\Http\Requests\StoreUrl
* @param int|string|null $userId Jika user_id tidak diisi, maka akan diisi
* null. Ini terjadi karena guest yang membuat
* URL. Lihat setUserIdAttribute().
* @return self
*/
public function shortenUrl(StoreUrl $request, $userId)
{
$key = $request->custom_key ?? $this->urlKey($request->long_url);
return Url::create([
'user_id' => $userId,
'destination' => $request->long_url,
'title' => $request->long_url,
'keyword' => $key,
'is_custom' => $request->custom_key ? true : false,
'ip' => Helper::anonymizeIp($request->ip()),
]);
}
/**
* @param int|string|null $userId \Illuminate\Contracts\Auth\Guard::id()
* @return bool \Illuminate\Database\Eloquent\Model::save()
*/
public function duplicate(string $key, $userId, string $randomKey = null)
{
$randomKey = $randomKey ?? $this->randomString();
$shortenedUrl = self::whereKeyword($key)->firstOrFail();
$replicate = $shortenedUrl->replicate()->fill([
'user_id' => $userId,
'keyword' => $randomKey,
'is_custom' => false,
]);
return $replicate->save();
}
public function urlKey(string $url): string
{
$length = config('urlhub.hash_length') * -1;
// Step 1
// Take a few characters at the end of the string to use as a unique key
$pattern = '/[^'.config('urlhub.hash_char').']/i';
$urlKey = substr(preg_replace($pattern, '', $url), $length);
// Step 2
// If step 1 fails (the already used or cannot be used), then the generator
// must generate a unique random string
if ($this->keyExists($urlKey)) {
$urlKey = $this->randomString();
}
return $urlKey;
}
/**
* Periksa apakah keyword tersedia atau tidak?
*
* Syarat keyword tersedia:
* - Tidak ada di database
* - Tidak ada di daftar config('urlhub.reserved_keyword')
* - Tidak digunakan oleh sistem sebagai rute
* The number of shortened URLs that have been created by each User
*/
private function keyExists(string $url): bool
public function numberOfUrls(int $userId): int
{
$route = \Illuminate\Routing\Route::class;
$routeCollection = \Illuminate\Support\Facades\Route::getRoutes()->get();
$routePath = array_map(fn ($route) => $route->uri, $routeCollection);
$isExistsInDb = self::whereKeyword($url)->first();
$isReservedKeyword = in_array($url, config('urlhub.reserved_keyword'));
$isRegisteredRoutePath = in_array($url, $routePath);
if ($isExistsInDb || $isReservedKeyword || $isRegisteredRoutePath) {
return true;
}
return false;
return self::whereUserId($userId)->count();
}
/**
* The number of unique random strings that have been used as the key for
* the long url that has been shortened
*
* Calculation formula:
* keyUsed = randomKey + customKey
*
* The character length of the generated for `customKey` should be similar
* to `randomKey`
* The total number of shortened URLs that have been created by guests
*/
public function keyUsed(): int
public function numberOfUrlsByGuests(): int
{
$hashLength = (int) config('urlhub.hash_length');
$regexPattern = '['.config('urlhub.hash_char').']{'.$hashLength.'}';
$randomKey = self::whereIsCustom(false)
->whereRaw('LENGTH(keyword) = ?', [$hashLength])
->count();
$customKey = self::whereIsCustom(true)
->whereRaw('LENGTH(keyword) = ?', [$hashLength])
->whereRaw("keyword REGEXP '".$regexPattern."'")
->count();
return $randomKey + $customKey;
return self::whereNull('user_id')->count();
}
/**
* Calculate the maximum number of unique random strings that can be
* generated
* Total shortened URLs created
*/
public function keyCapacity(): int
public function totalUrl(): int
{
$alphabet = strlen(config('urlhub.hash_char'));
$length = config('urlhub.hash_length');
// for testing purposes only
// tests\Unit\Middleware\UrlHubLinkCheckerTest.php
if ($length === 0) {
return 0;
}
return (int) pow($alphabet, $length);
return self::count();
}
/**
* Count unique random strings that can be generated
* Total clicks on each shortened URLs
*/
public function keyRemaining(): int
{
$keyCapacity = $this->keyCapacity();
$keyUsed = $this->keyUsed();
return max($keyCapacity - $keyUsed, 0);
}
public function keyRemainingInPercent(int $precision = 2): string
public function numberOfClicks(int $urlId, bool $unique = false): int
{
$capacity = $this->keyCapacity();
$remaining = $this->keyRemaining();
$result = round(($remaining / $capacity) * 100, $precision);
$lowerBoundInPercent = 1 / (10 ** $precision);
$upperBoundInPercent = 100 - $lowerBoundInPercent;
$lowerBound = $lowerBoundInPercent / 100;
$upperBound = 1 - $lowerBound;
$total = self::find($urlId)->visit()->count();
if ($remaining > 0 && $remaining < ($capacity * $lowerBound)) {
$result = $lowerBoundInPercent;
} elseif (($remaining > ($capacity * $upperBound)) && ($remaining < $capacity)) {
$result = $upperBoundInPercent;
if ($unique) {
$total = self::find($urlId)->visit()
->whereIsFirstClick(true)
->count();
}
return $result.'%';
return $total;
}
/**
* Count the number of URLs based on user id.
*
* @param int|string|null $userId
* Total clicks on all short URLs on each user
*/
public function urlCount($userId = null): int
public function numberOfClicksPerUser(int $userId = null): int
{
return self::whereUserId($userId)->count('keyword');
}
$url = self::whereUserId($userId)->get();
public function totalUrl(): int
{
return self::count('keyword');
return $url->sum(fn ($url) => $url->numberOfClicks($url->id));
}
/**
* Fetch the page title from the web page URL
*
* @throws \Exception
* Total clicks on all short URLs from guest users
*/
public function getWebTitle(string $url): string
public function numberOfClicksFromGuests(): int
{
$spatieUrl = SpatieUrl::fromString($url);
$defaultTitle = $spatieUrl->getHost().' - Untitled';
try {
$webTitle = (new Embed)->get($url)->title ?? $defaultTitle;
} catch (\Exception) {
// If failed or not found, then return "{domain_name} - Untitled"
$webTitle = $defaultTitle;
}
$url = self::whereNull('user_id')->get();
return $webTitle;
return $url->sum(fn ($url) => $url->numberOfClicks($url->id));
}
/**
* @return string
* Total clicks on all shortened URLs
*/
public function randomString()
public function totalClick(): int
{
$factory = new \RandomLib\Factory;
$generator = $factory->getMediumStrengthGenerator();
$character = config('urlhub.hash_char');
$length = config('urlhub.hash_length');
do {
$urlKey = $generator->generateString($length, $character);
} while ($this->keyExists($urlKey));
return $urlKey;
return Visit::count();
}
}

@ -64,11 +64,16 @@ class User extends Authenticatable
|--------------------------------------------------------------------------
*/
public function totalUsers(): int
{
return self::count();
}
/*
* Count the number of guests (URL without user id) by IP address, then
* grouped by IP address.
*/
public function guestCount(): int
public function totalGuestUsers(): int
{
$url = Url::select('ip', DB::raw('count(*) as total'))
->whereNull('user_id')->groupBy('ip')

@ -16,7 +16,6 @@ class Visit extends Model
*/
protected $fillable = [
'url_id',
'url_author_id',
'visitor_id',
'is_first_click',
'referer',
@ -50,70 +49,4 @@ class Visit extends Model
{
return $this->belongsTo(Url::class);
}
/*
|--------------------------------------------------------------------------
| General Functions
|--------------------------------------------------------------------------
*/
/**
* Generate unique Visitor Id
*/
public function visitorId(): string
{
$neighborVisitor = [
'ip' => request()->ip(),
'browser' => \Browser::browserFamily(),
'os' => \Browser::platformFamily(),
];
$visitorId = hash('sha3-256', implode($neighborVisitor));
if (auth()->check() === true) {
$visitorId = (string) auth()->id();
}
return $visitorId;
}
public function isFirstClick(Url $url): bool
{
$hasVisited = Visit::whereVisitorId($this->visitorId())
->whereUrlId($url->id)
->first();
return $hasVisited ? false : true;
}
/**
* total visit
*/
public function totalClick(): int
{
return self::count();
}
/**
* Total visit by user id
*/
public function totalClickPerUser(int $authorId = null): int
{
return self::whereUrlAuthorId($authorId)->count();
}
/**
* Total visit by URL id
*/
public function totalClickPerUrl(int $urlId, bool $unique = false): int
{
$total = self::whereUrlId($urlId)->count();
if ($unique) {
$total = self::whereUrlId($urlId)
->whereIsFirstClick(true)
->count();
}
return $total;
}
}

@ -2,8 +2,8 @@
namespace App\Providers;
use App\Actions\Fortify\CreateNewUser;
use App\Models\User;
use App\Services\Fortify\CreateNewUser;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

@ -2,8 +2,8 @@
namespace App\Rules\Url;
use App\Services\KeyGeneratorService;
use Illuminate\Contracts\Validation\InvokableRule;
use Illuminate\Routing\Route;
/**
* Check if keyword id is free (ie not already taken, not a URL path, and not
@ -21,28 +21,10 @@ class KeywordBlacklist implements InvokableRule
*/
public function __invoke($attribute, $value, $fail)
{
if (! $this->isProbihitedKeyword($value)) {
$fail('Not available.');
}
}
/**
* @param mixed $value
*/
protected function isProbihitedKeyword($value): bool
{
$routes = array_map(
fn (Route $route) => $route->uri,
\Route::getRoutes()->get()
);
$isReservedKeyword = in_array($value, config('urlhub.reserved_keyword'), true);
$isRegisteredRoute = in_array($value, $routes);
$stringCanBeUsedAsKey = app(KeyGeneratorService::class)->assertStringCanBeUsedAsKey($value);
if ($isRegisteredRoute || $isReservedKeyword) {
return false;
if ($stringCanBeUsedAsKey === false) {
$fail('Not available.');
}
return true;
}
}

@ -1,6 +1,6 @@
<?php
namespace App\Actions\Fortify;
namespace App\Services\Fortify;
use App\Models\User;
use Illuminate\Support\Facades\Hash;

@ -0,0 +1,179 @@
<?php
namespace App\Services;
use App\Models\Url;
class KeyGeneratorService
{
/**
* Generate a short string that can be used as a unique key for shortened long
* urls.
*
* @return string A unique string to use as the short url key
*/
public function urlKey(string $value): string
{
// Step 1
$key = $this->generateSimpleString($value);
// Step 2
// If step 1 fail (the string is used or cannot be used), then the generator
// must generate a unique random string until it finds a string that can
// be used as a key
if ($this->assertStringCanBeUsedAsKey($key) === false) {
$key = $this->generateRandomString();
}
return $key;
}
/**
* Take some characters at the end of the string and remove all characters that
* are not in the specified character set.
*/
public function generateSimpleString(string $value): string
{
$length = config('urlhub.hash_length') * -1;
$pattern = '/[^'.config('urlhub.hash_char').']/i';
return substr(preg_replace($pattern, '', $value), $length);
}
/**
* Generate a random string of specified length. The string will only contain
* characters from the specified character set.
*
* @return string The generated random string
*/
public function generateRandomString()
{
$factory = new \RandomLib\Factory;
$generator = $factory->getMediumStrengthGenerator();
$characters = config('urlhub.hash_char');
$length = config('urlhub.hash_length');
do {
$urlKey = $generator->generateString($length, $characters);
} while ($this->assertStringCanBeUsedAsKey($urlKey) === false);
return $urlKey;
}
/**
* Check if string can be used as a keyword.
*
* This function will check under several conditions:
* 1. If the string is already used in the database
* 2. If the string is used as a reserved keyword
* 3. If the string is used as a route path
*
* If any or all of the conditions are true, then the keyword cannot be used.
*/
public function assertStringCanBeUsedAsKey(string $value): bool
{
$route = \Illuminate\Routing\Route::class;
$routeCollection = \Illuminate\Support\Facades\Route::getRoutes()->get();
$routePath = array_map(fn ($route) => $route->uri, $routeCollection);
$isExistsInDb = Url::whereKeyword($value)->exists();
$isReservedKeyword = in_array($value, config('urlhub.reserved_keyword'));
$isRegisteredRoutePath = in_array($value, $routePath);
if ($isExistsInDb || $isReservedKeyword || $isRegisteredRoutePath) {
return false;
}
return true;
}
/*
|--------------------------------------------------------------------------
| Capacity calculation
|--------------------------------------------------------------------------
*/
/**
* Calculate the maximum number of unique random strings that can be
* generated
*/
public function maxCapacity(): int
{
$characters = strlen(config('urlhub.hash_char'));
$length = config('urlhub.hash_length');
// for testing purposes only
// tests\Unit\Middleware\UrlHubLinkCheckerTest.php
if ($length === 0) {
return 0;
}
return (int) pow($characters, $length);
}
/**
* The number of unique random strings that have been used as the key for
* the long url that has been shortened
*
* Formula:
* usedCapacity = randomKey + customKey
*
* The character length and set of characters of `customKey` must be the same
* as `randomKey`.
*/
public function usedCapacity(): int
{
$hashLength = (int) config('urlhub.hash_length');
$regexPattern = '['.config('urlhub.hash_char').']{'.$hashLength.'}';
$randomKey = Url::whereIsCustom(false)
->whereRaw('LENGTH(keyword) = ?', [$hashLength])
->count();
$customKey = Url::whereIsCustom(true)
->whereRaw('LENGTH(keyword) = ?', [$hashLength])
->whereRaw("keyword REGEXP '".$regexPattern."'")
->count();
return $randomKey + $customKey;
}
/**
* Calculate the number of unique random strings that can still be generated.
*/
public function idleCapacity(): int
{
$maxCapacity = $this->maxCapacity();
$usedCapacity = $this->usedCapacity();
// prevent negative values
return max($maxCapacity - $usedCapacity, 0);
}
/**
* Calculate the percentage of the remaining unique random strings that can
* be generated from the total number of unique random strings that can be
* generated (in percent) with the specified precision (in decimal places)
* and return the result as a string.
*/
public function idleCapacityInPercent(int $precision = 2): string
{
$maxCapacity = $this->maxCapacity();
$remaining = $this->idleCapacity();
$result = round(($remaining / $maxCapacity) * 100, $precision);
$lowerBoundInPercent = 1 / (10 ** $precision);
$upperBoundInPercent = 100 - $lowerBoundInPercent;
$lowerBound = $lowerBoundInPercent / 100;
$upperBound = 1 - $lowerBound;
if ($remaining > 0 && $remaining < ($maxCapacity * $lowerBound)) {
$result = $lowerBoundInPercent;
} elseif (($remaining > ($maxCapacity * $upperBound)) && ($remaining < $maxCapacity)) {
$result = $upperBoundInPercent;
}
return $result.'%';
}
}

@ -1,11 +1,11 @@
<?php
namespace App\Actions;
namespace App\Services;
use Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\Writer\Result\ResultInterface;
class QrCodeAction
class QrCodeService
{
const MIN_SIZE = 50;
@ -15,7 +15,7 @@ class QrCodeAction
const SUPPORTED_FORMAT = ['png', 'svg'];
public function process(string $data): ResultInterface
public function execute(string $data): ResultInterface
{
return Builder::create()
->data($data)

@ -0,0 +1,106 @@
<?php
namespace App\Services;
use App\Http\Requests\StoreUrl;
use App\Models\Url;
use Embed\Embed;
use Illuminate\Http\Request;
use Spatie\Url\Url as SpatieUrl;
class UHubLinkService
{
/** @readonly */
public string $new_keyword;
public function __construct(
public KeyGeneratorService $keyGeneratorService,
) {
$this->new_keyword = $keyGeneratorService->generateRandomString();
}
/**
* Create a shortened URL.
*
* @param StoreUrl $request \App\Http\Requests\StoreUrl
*/
public function create(StoreUrl $request): Url
{
return Url::create([
'user_id' => auth()->id(),
'destination' => $request->long_url,
'title' => $this->title($request->long_url),
'keyword' => $this->urlKey($request),
'is_custom' => $this->isCustom($request),
'ip' => $request->ip(),
]);
}
/**
* Update the shortened URL details.
*
* @param Request $request \Illuminate\Http\Request
* @return bool \Illuminate\Database\Eloquent\Model::update()
*/
public function update(Request $request, Url $url)
{
return $url->update([
'destination' => $request->long_url,
'title' => $request->title,
]);
}
/**
* Duplicate the existing URL and create a new shortened URL.
*
* @param string $urlKey A unique key for the shortened URL
* @return bool \Illuminate\Database\Eloquent\Model::save()
*/
public function duplicate(string $urlKey)
{
$shortenedUrl = Url::whereKeyword($urlKey)->first();
$replicate = $shortenedUrl->replicate()->fill([
'user_id' => auth()->id(),
'keyword' => $this->new_keyword,
'is_custom' => false,
]);
return $replicate->save();
}
/**
* Fetch the page title from the web page URL
*
* @throws \Exception
*/
public function title(string $webAddress): string
{
$spatieUrl = SpatieUrl::fromString($webAddress);
$defaultTitle = $spatieUrl->getHost().' - Untitled';
if (config('urlhub.web_title')) {
try {
$title = app(Embed::class)->get($webAddress)->title ?? $defaultTitle;
} catch (\Exception) {
// If failed or not found, then return "{domain_name} - Untitled"
$title = $defaultTitle;
}
return $title;
}
return 'No Title';
}
private function urlKey(StoreUrl $request): string
{
return $request->custom_key ??
$this->keyGeneratorService->urlKey($request->long_url);
}
private function isCustom(StoreUrl $request): bool
{
return $request->custom_key ? true : false;
}
}

@ -0,0 +1,25 @@
<?php
namespace App\Services;
use App\Models\Url;
class UrlRedirection
{
/**
* Execute the HTTP redirect and return the redirect response.
*
* @param Url $url \App\Models\Url
* @return \Illuminate\Http\RedirectResponse
*/
public function execute(Url $url)
{
$statusCode = (int) config('urlhub.redirect_status_code');
$maxAge = (int) config('urlhub.redirect_cache_max_age');
$headers = [
'Cache-Control' => sprintf('private,max-age=%s', $maxAge),
];
return redirect()->away($url->destination, $statusCode, $headers);
}
}

@ -0,0 +1,89 @@
<?php
namespace App\Services;
use App\Models\Url;
use App\Models\Visit;
class VisitorService
{
/**
* Store the visitor data.
*
* @param Url $url \App\Models\Url
* @return void
*/
public function create(Url $url)
{
$logBotVisit = config('urlhub.track_bot_visits');
if ($logBotVisit === false && \Browser::isBot() === true) {
return;
}
Visit::create([
'url_id' => $url->id,
'visitor_id' => $this->visitorId(),
'is_first_click' => $this->isFirstClick($url),
'referer' => request()->header('referer'),
'ip' => request()->ip(),
'browser' => \Browser::browserFamily(),
'browser_version' => \Browser::browserVersion(),
'device' => \Browser::deviceType(),
'os' => \Browser::platformFamily(),
'os_version' => \Browser::platformVersion(),
]);
}
/**
* Generate unique Visitor Id
*/
public function visitorId(): string
{
$visitorId = $this->authVisitorId();
if ($this->isAnonymousVisitor()) {
$visitorId = $this->anonymousVisitorId();
}
return $visitorId;
}
public function authVisitorId(): string
{
return (string) auth()->id();
}
public function anonymousVisitorId(): string
{
$data = [
'ip' => request()->ip(),
'browser' => \Browser::browserFamily(),
'os' => \Browser::platformFamily(),
];
return sha1(implode($data));
}
/**
* Check if the visitor is an anonymous (unauthenticated) visitor.
*/
public function isAnonymousVisitor(): bool
{
return auth()->check() === false;
}
/**
* Check if the visitor has clicked the link before. If the visitor has not
* clicked the link before, return true.
*
* @param Url $url \App\Models\Url
*/
public function isFirstClick(Url $url): bool
{
$hasVisited = Visit::whereUrlId($url->id)
->whereVisitorId($this->visitorId())
->exists();
return $hasVisited ? false : true;
}
}

@ -76,16 +76,6 @@ return [
|--------------------------------------------------------------------------
*/
/*
* Tells if IP addresses from visitors should be obfuscated before storing
* them in the database.
*
* Be careful!
* Setting this to false will make your UrlHub instance no longer be in
* compliance with the GDPR and other similar data protection regulations.
*/
'anonymize_ip_addr' => env('UH_ANONYMIZE_IP_ADDR', true),
/*
* Configure the kind of redirect you want to use for your short URLs. You
* can either set:

@ -4,6 +4,7 @@ namespace Database\Factories;
use App\Models\Url;
use App\Models\User;
use App\Services\KeyGeneratorService;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
@ -29,9 +30,9 @@ class UrlFactory extends Factory
'user_id' => User::factory(),
'destination' => 'https://github.com/realodix/urlhub',
'title' => 'No Title',
'keyword' => (new Url)->randomString(),
'keyword' => app(KeyGeneratorService::class)->generateRandomString(),
'is_custom' => false,
'ip' => $this->faker->ipv4(),
'ip' => fake()->ipv4(),
];
}
}

@ -30,7 +30,7 @@ class VisitFactory extends Factory
'visitor_id' => 'foo_bar',
'is_first_click' => true,
'referer' => 'https://github.com/realodix/urlhub',
'ip' => $this->faker->ipv4(),
'ip' => fake()->ipv4(),
'browser' => 'Firefox',
'browser_version' => '108',
'device' => 'Desktop',

@ -18,14 +18,10 @@ return new class extends Migration
$table->foreignId('url_id')
->constrained()
->cascadeOnDelete();
$table->foreignId('url_author_id')
->nullable()
->constrained('users')
->cascadeOnDelete();
$table->string('visitor_id');
$table->boolean('is_first_click');
$table->string('referer', 300)->nullable()->default(0);
$table->ipAddress('ip');
$table->string('referer', 300)->nullable();
$table->ipAddress('ip')->nullable();
$table->string('browser')->nullable();
$table->string('browser_version')->nullable();
$table->string('device')->nullable();

@ -14,8 +14,8 @@
</div>
<div class="mt-8 sm:mt-0 text-uh-1 ">
<b>@svg('icon-storage', 'mr-1.5') {{__('Free Space')}}:</b>
<span class="font-light">{{compactNumber($url->keyRemaining())}} {{__('of')}}
{{compactNumber($url->keyCapacity())}} ({{$url->keyRemainingInPercent()}})
<span class="font-light">{{compactNumber($keyGeneratorService->idleCapacity())}} {{__('of')}}
{{compactNumber($keyGeneratorService->maxCapacity())}} ({{$keyGeneratorService->idleCapacityInPercent()}})
</span>
</div>
</div>
@ -25,24 +25,24 @@
<div class="block">
<b class="text-uh-1">@svg('icon-link', 'mr-1.5') {{__('URLs')}}:</b>
<span class="text-cyan-600">{{compactNumber($url->totalUrl())}}</span> -
<span class="text-teal-600">{{compactNumber($url->urlCount(auth()->id()))}}</span> -
<span class="text-orange-600">{{compactNumber($url->urlCount())}}</span>
<span class="text-teal-600">{{compactNumber($url->numberOfUrls(auth()->id()))}}</span> -
<span class="text-orange-600">{{compactNumber($url->numberOfUrlsByGuests())}}</span>
</div>
<div class="block">
<b class="text-uh-1">@svg('icon-bar-chart', 'mr-1.5') {{__('Clicks')}}:</b>
<span class="text-cyan-600">{{compactNumber($visit->totalClick())}}</span> -
<span class="text-teal-600">{{compactNumber($visit->totalClickPerUser(auth()->id()))}}</span> -
<span class="text-orange-600">{{compactNumber($visit->totalClickPerUser())}}</span>
<span class="text-cyan-600">{{compactNumber($url->totalClick())}}</span> -
<span class="text-teal-600">{{compactNumber($url->numberOfClicksPerUser(auth()->id()))}}</span> -
<span class="text-orange-600">{{compactNumber($url->numberOfClicksFromGuests())}}</span>
</div>
</div>
<div class="text-uh-1 w-full sm:w-1/4 mt-4 sm:mt-0">
<div class="block">
<b>@svg('icon-user', 'mr-1.5') {{__('Users')}}:</b>
<span class="font-light">{{compactNumber($user->count())}}</span>
<span class="font-light">{{compactNumber($user->totalUsers())}}</span>
</div>
<div class="block">
<b>@svg('icon-user', 'mr-1.5') {{__('Guests')}}:</b>
<span class="font-light">{{compactNumber($user->guestCount())}}</span>
<span class="font-light">{{compactNumber($user->totalGuestUsers())}}</span>
</div>
</div>
</div>
@ -50,11 +50,11 @@
<div class="flex flex-wrap">
<div class="w-full sm:w-1/4">
<span class="font-semibold text-md sm:text-2xl">@svg('icon-link', 'mr-1.5') {{__('URLs')}}:</span>
<span class="font-light text-lg sm:text-2xl">{{compactNumber($url->urlCount(auth()->id()))}}</span>
<span class="font-light text-lg sm:text-2xl">{{compactNumber($url->numberOfUrls(auth()->id()))}}</span>
</div>
<div class="w-full sm:w-1/4">
<span class="font-semibold text-lg sm:text-2xl">@svg('icon-eye', 'mr-1.5') {{__('Clicks')}}:</span>
<span class="font-light text-lg sm:text-2xl">{{compactNumber($visit->totalClickPerUser(auth()->id()))}}</span>
<span class="font-light text-lg sm:text-2xl">{{compactNumber($url->numberOfClicksPerUser(auth()->id()))}}</span>
</div>
</div>
@endrole

@ -16,8 +16,8 @@
<li class="inline-block pr-4 mt-4 lg:mt-0">
@svg('icon-bar-chart')
<i>
<span title="{{number_format($visit->totalClickPerUrl($url->id))}}" class="font-bold">
{{compactNumber($visit->totalClickPerUrl($url->id))}}
<span title="{{number_format($url->clicks)}}" class="font-bold">
{{compactNumber($url->clicks)}}
</span>
</i>
{{__('Total engagements')}}

@ -19,9 +19,8 @@
<div class="block pl-3 pr-4 py-4 font-medium text-base text-orange-700 bg-orange-50 border-l-4 border-orange-400">
{{ session('msgLinkAlreadyExists') }}
{{__('Do you want to duplicate this link?')}}
@auth
{{__('Do you want to duplicate this link?')}}
<div class="mt-4 ">
<a href="{{route('su_duplicate', $url->keyword)}}"
class="btn-icon-detail !bg-[#007c8c] hover:!bg-[#00525f] !text-white"

@ -1,6 +1,6 @@
<?php
use Illuminate\Support\Facades\Route;
// use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
@ -12,5 +12,3 @@ use Illuminate\Support\Facades\Route;
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
Route::post('/url', 'UrlController@store');

@ -1,59 +0,0 @@
<?php
namespace Tests\Feature\API;
use Illuminate\Http\Response;
use Tests\TestCase;
class UrlTest extends TestCase
{
/**
* @test
* @group f-api
*/
public function canCreateUrl()
{
$data = [
'long_url' => 'http://example.com',
];
$this->json('POST', '/api/url', $data)
->assertStatus(Response::HTTP_CREATED)
->assertJsonStructure([
'id',
'long_url',
'short_url',
]);
$this->assertDatabaseHas('urls', [
'destination' => 'http://example.com',
]);
}
/**
* @test
* @group f-api
* @dataProvider shortenUrlFailProvider
*
* @param mixed $value
*/
public function shortenUrlFail($value)
{
$data = [
'long_url' => $value,
];
$this->json('POST', '/api/url', $data)
->assertJsonStructure([
'errors',
]);
}
public function shortenUrlFailProvider()
{
return [
[''],
['foobar.com'],
];
}
}

@ -2,19 +2,24 @@
namespace Tests\Unit\Actions;
use App\Actions\QrCodeAction;
use App\Services\QrCodeService;
use Endroid\QrCode\Writer\Result\ResultInterface;
use Tests\TestCase;
class QrCodeTest extends TestCase
{
private function getQrCode(): QrCodeService
{
return app(QrCodeService::class);
}
/**
* @test
* @group u-actions
*/
public function QrCodeAction()
public function QrCodeService()
{
$QrCode = (new QrCodeAction)->process('foo');
$QrCode = $this->getQrCode()->execute('foo');
$this->assertInstanceOf(ResultInterface::class, $QrCode);
}
@ -25,13 +30,13 @@ class QrCodeTest extends TestCase
*/
public function sizeMin()
{
$size = QrCodeAction::MIN_SIZE - 1;
$size = QrCodeService::MIN_SIZE - 1;
config(['urlhub.qrcode_size' => $size]);
$image = imagecreatefromstring((new QrCodeAction)->process('foo')->getString());
$image = imagecreatefromstring($this->getQrCode()->execute('foo')->getString());
$this->assertNotSame($size, (int) imagesx($image));
$this->assertSame(QrCodeAction::MIN_SIZE, imagesx($image));
$this->assertSame(QrCodeService::MIN_SIZE, imagesx($image));
}
/**
@ -40,12 +45,12 @@ class QrCodeTest extends TestCase
*/
public function sizeMax()
{
$size = QrCodeAction::MAX_SIZE + 1;
$size = QrCodeService::MAX_SIZE + 1;
config(['urlhub.qrcode_size' => $size]);
$image = imagecreatefromstring((new QrCodeAction)->process('foo')->getString());
$image = imagecreatefromstring($this->getQrCode()->execute('foo')->getString());
$this->assertNotSame($size, imagesx($image));
$this->assertSame(QrCodeAction::MAX_SIZE, imagesx($image));
$this->assertSame(QrCodeService::MAX_SIZE, imagesx($image));
}
}

@ -7,28 +7,6 @@ use Tests\TestCase;
class HelperTest extends TestCase
{
public function testAnonymizeIpWhenConfigSettedTrue()
{
config()->set('urlhub.anonymize_ip_addr', true);
$ip = '192.168.1.1';
$expected = Helper::anonymizeIp($ip);
$actual = '192.168.1.0';
$this->assertSame($expected, $actual);
}
public function testAnonymizeIpWhenConfigSettedFalse()
{
config()->set('urlhub.anonymize_ip_addr', false);
$ip = '192.168.1.1';
$expected = Helper::anonymizeIp($ip);
$actual = $ip;
$this->assertSame($expected, $actual);
}
/**
* @test
*/

@ -25,12 +25,12 @@ class UrlHubLinkCheckerTest extends TestCase
}
/**
* Shorten the url when the random string generator capacity is full.
* Shorten the url when the random string generator maxCapacity is full.
* UrlHub must prevent URL shortening.
*
* @test
*/
public function keyRemainingZero()
public function idleCapacityZero()
{
config(['urlhub.hash_length' => 0]);

@ -150,328 +150,144 @@ class UrlTest extends TestCase
}
/**
* String yang dihasilkan dengan memotong string url dari belakang sepanjang
* panjang karakter yang telah ditentukan.
*
* @test
* @group u-model
*/
public function urlKey_default_value()
public function totalShortUrl()
{
$length = 3;
config(['urlhub.hash_length' => $length]);
$longUrl = 'https://github.com/realodix';
$urlKey = $this->url->urlKey($longUrl);
$expected = $this->totalUrl;
$actual = $this->url->totalUrl();
$this->assertSame(substr($longUrl, -$length), $urlKey);
$this->assertSame($expected, $actual);
}
/**
* Karena kunci sudah ada, maka generator akan terus diulangi hingga
* menghasilkan kunci yang unik atau tidak ada yang sama.
*
* @test
* @group u-model
*/
public function urlKey_generated_string()
public function totalShortUrlByMe()
{
$length = 3;
config(['urlhub.hash_length' => $length]);
$longUrl = 'https://github.com/realodix';
Url::factory()->create([
'keyword' => $this->url->urlKey($longUrl),
]);
$this->assertNotSame(substr($longUrl, -$length), $this->url->urlKey($longUrl));
}
$expected = self::N_URL_WITH_USER_ID;
$actual = $this->url->numberOfUrls($this->admin()->id);
/**
* Panjang dari karakter kunci yang dihasilkan harus sama dengan panjang
* karakter yang telah ditentukan.
*
* @test
* @group u-model
*/
public function urlKey_specified_hash_length()
{
config(['urlhub.hash_length' => 6]);
$actual = 'https://github.com/realodix';
$expected = 'alodix';
$this->assertSame($expected, $this->url->urlKey($actual));
config(['urlhub.hash_length' => 9]);
$actual = 'https://github.com/realodix';
$expected = 'mrealodix';
$this->assertSame($expected, $this->url->urlKey($actual));
config(['urlhub.hash_length' => 12]);
$actual = 'https://github.com/realodix';
$expected = 'bcomrealodix';
$this->assertSame($expected, $this->url->urlKey($actual));
$this->assertSame($expected, $actual);
}
/**
* Karakter yang dihasilkan harus benar-benar mengikuti karakter yang telah
* ditentukan.
*
* @test
* @group u-model
*/
public function urlKey_specified_character()
public function totalShortUrlByGuest()
{
$url = 'https://example.com/abc';
config(['urlhub.hash_length' => 3]);
$this->assertSame('abc', $this->url->urlKey($url));
config(['urlhub.hash_char' => 'xyz']);
$this->assertMatchesRegularExpression('/[xyz]/', $this->url->urlKey($url));
$this->assertDoesNotMatchRegularExpression('/[abc]/', $this->url->urlKey($url));
$expected = self::N_URL_WITHOUT_USER_ID;
$actual = $this->url->numberOfUrlsByGuests();
config(['urlhub.hash_length' => 4]);
config(['urlhub.hash_char' => 'abcm']);
$this->assertSame('mabc', $this->url->urlKey($url));
$this->assertSame($expected, $actual);
}
/**
* String yang dihasilkan tidak boleh sama dengan string yang telah ada di
* config('urlhub.reserved_keyword')
*
* @test
* @group u-model
*/
public function urlKey_prevent_reserved_keyword()
public function totalClicks()
{
$actual = 'https://example.com/css';
$expected = 'css';
Visit::factory()->create();
config(['urlhub.reserved_keyword' => [$expected]]);
config(['urlhub.hash_length' => strlen($expected)]);
$url = new Url;
$this->assertNotSame($expected, $this->url->urlKey($actual));
}
$expected = 1;
$actual = $url->totalClick();
/**
* String yang dihasilkan tidak boleh sama dengan string yang telah ada di
* registered route path. Di sini, string yang dihasilkan sebagai keyword
* adalah 'admin', dimana 'admin' sudah digunakan sebagai route path.
*
* @test
* @group u-model
*/
public function urlKey_prevent_generating_strings_that_are_in_registered_route_path()
{
$actual = 'https://example.com/admin';
$expected = 'admin';
config(['urlhub.hash_length' => strlen($expected)]);
$this->assertNotSame($expected, $this->url->urlKey($actual));
$this->assertSame($expected, $actual);
}
/**
* Pengujian dilakukan berdasarkan panjang karakternya.
*
* @test
* @group u-model
*/
public function keyUsed()
public function numberOfClicks()
{
config(['urlhub.hash_length' => config('urlhub.hash_length') + 1]);
Url::factory()->create([
'keyword' => $this->url->randomString(),
Visit::factory()->create([
'url_id' => 1,
'is_first_click' => true,
]);
$this->assertSame(1, $this->url->keyUsed());
Url::factory()->create([
'keyword' => str_repeat('a', config('urlhub.hash_length')),
'is_custom' => true,
Visit::factory()->create([
'url_id' => 1,
'is_first_click' => false,
]);
$this->assertSame(2, $this->url->keyUsed());
// Karena panjang karakter 'keyword' berbeda dengan dengan 'urlhub.hash_length',
// maka ini tidak ikut terhitung.
Url::factory()->create([
'keyword' => str_repeat('b', config('urlhub.hash_length') + 2),
'is_custom' => true,
]);
$this->assertSame(2, $this->url->keyUsed());
$expected = 2;
$actual = $this->url->numberOfClicks(1);
config(['urlhub.hash_length' => config('urlhub.hash_length') + 3]);
$this->assertSame(0, $this->url->keyUsed());
$this->assertSame($this->totalUrl + 3, $this->url->totalUrl());
$this->assertSame($expected, $actual);
}
/**
* Pengujian dilakukan berdasarkan karakter yang telah ditetapkan pada
* 'urlhub.hash_char'. Jika salah satu karakter 'keyword' tidak ada di
* 'urlhub.hash_char', maka seharusnya ini tidak dapat dihitung.
*
* @test
* @group u-model
*/
public function keyUsed2()
public function numberOfClicksAndUnique()
{
config(['urlhub.hash_length' => 3]);
config(['urlhub.hash_char' => 'foo']);
Url::factory()->create([
'keyword' => 'foo',
'is_custom' => true,
Visit::factory()->create([
'url_id' => 1,
'is_first_click' => true,
]);
$this->assertSame(1, $this->url->keyUsed());
config(['urlhub.hash_char' => 'bar']);
Url::factory()->create([
'keyword' => 'bar',
'is_custom' => true,
Visit::factory()->create([
'url_id' => 1,
'is_first_click' => false,
]);
$this->assertSame(1, $this->url->keyUsed());
// Sudah ada 2 URL yang dibuat dengan keyword 'foo' dan 'bar', maka
// seharusnya ada 2 saja.
config(['urlhub.hash_char' => 'foobar']);
$this->assertSame(2, $this->url->keyUsed());
// Sudah ada 2 URL yang dibuat dengan keyword 'foo' dan 'bar', maka
// seharusnya ada 1 saja karena 'bar' tidak bisa terhitung.
config(['urlhub.hash_char' => 'fooBar']);
$this->assertSame(1, $this->url->keyUsed());
// Sudah ada 2 URL yang dibuat dengan keyword 'foo' dan 'bar', maka
// seharusnya tidak ada sama sekali karena 'foo' dan 'bar' tidak
// bisa terhitung.
config(['urlhub.hash_char' => 'FooBar']);
$this->assertSame(0, $this->url->keyUsed());
}
/**
* @test
* @group u-model
*/
public function keyCapacity()
{
$hashLength = config('urlhub.hash_length');
$hashCharLength = strlen(config('urlhub.hash_char'));
$keyCapacity = pow($hashCharLength, $hashLength);
$this->assertSame($keyCapacity, $this->url->keyCapacity());
}
/**
* @test
* @group u-model
* @dataProvider keyRemainingProvider
*
* @param mixed $kc
* @param mixed $nouk
* @param mixed $expected
*/
public function keyRemaining($kc, $nouk, $expected)
{
$mock = \Mockery::mock(Url::class)->makePartial();
$mock->shouldReceive([
'keyCapacity' => $kc,
'keyUsed' => $nouk,
]);
$actual = $mock->keyRemaining();
$expected = 1;
$actual = $this->url->numberOfClicks(1, unique: true);
$this->assertSame($expected, $actual);
}
public function keyRemainingProvider()
{
// keyCapacity(), keyUsed(), expected_result
return [
[1, 2, 0],
[3, 2, 1],
];
}
/**
* @test
* @group u-model
* @dataProvider keyRemainingInPercentProvider
* Total klik dari setiap shortened URLs yang dibuat oleh user tertentu
*
* @param mixed $kc
* @param mixed $nouk
* @param mixed $expected
*/
public function keyRemainingInPercent($kc, $nouk, $expected)
{
$mock = \Mockery::mock(Url::class)->makePartial();
$mock->shouldReceive([
'keyCapacity' => $kc,
'keyUsed' => $nouk,
]);
$actual = $mock->keyRemainingInPercent();
$this->assertSame($expected, $actual);
}
public function keyRemainingInPercentProvider()
{
// keyCapacity(), keyUsed(), expected_result
return [
[10, 10, '0%'],
[10, 11, '0%'],
[pow(10, 6), 999991, '0.01%'],
[pow(10, 6), 50, '99.99%'],
[pow(10, 6), 0, '100%'],
];
}
/**
* @test
* @group u-model
*/
public function totalShortUrl()
public function numberOfClicksPerUser()
{
$expected = $this->totalUrl;
$actual = $this->url->totalUrl();
$this->assertSame($expected, $actual);
}
$userId = $this->admin()->id;
$url = Url::factory()->create([
'user_id' => $userId,
]);
Visit::factory()->create([
'url_id' => $url->id,
]);
/**
* @test
* @group u-model
*/
public function totalShortUrlByMe()
{
$expected = self::N_URL_WITH_USER_ID;
$actual = $this->url->urlCount($this->admin()->id);
$expected = Visit::whereUrlId($url->id)->count();
$actual = $this->url->numberOfClicksPerUser(userId: $url->user_id);
$this->assertSame($userId, $url->user_id);
$this->assertSame($expected, $actual);
}
/**
* Total klik dari setiap shortened URLs yang dibuat oleh guest user
*
* @test
* @group u-model
*/
public function totalShortUrlByGuest()
public function numberOfClicksFromGuests()
{
$expected = self::N_URL_WITHOUT_USER_ID;
$actual = $this->url->urlCount();
$this->assertSame($expected, $actual);
}
$userId = null;
$url = Url::factory()->create([
'user_id' => $userId,
]);
Visit::factory()->create([
'url_id' => $url->id,
]);
/**
* @test
* @group u-model
*/
public function getWebTitle()
{
$expected = 'example123456789.com - Untitled';
$actual = $this->url->getWebTitle('https://example123456789.com');
$this->assertSame($expected, $actual);
$expected = Visit::whereUrlId($url->id)->count();
$actual = $this->url->numberOfClicksFromGuests();
$expected = 'www.example123456789.com - Untitled';
$actual = $this->url->getWebTitle('https://www.example123456789.com');
$this->assertSame($userId, $url->user_id);
$this->assertSame($expected, $actual);
}
}

@ -29,8 +29,8 @@ class UserTest extends TestCase
* @test
* @group u-model
*/
public function guestCount()
public function totalGuestUsers()
{
$this->assertSame(0, (new User)->guestCount());
$this->assertSame(0, (new User)->totalGuestUsers());
}
}

@ -8,13 +8,9 @@ use Tests\TestCase;
class VisitTest extends TestCase
{
private Visit $visit;
protected function setUp(): void
{
parent::setUp();
$this->visit = new Visit;
}
/**
@ -29,96 +25,4 @@ class VisitTest extends TestCase
$this->assertTrue($visit->url()->exists());
}
/**
* @test
* @group u-model
*/
public function totalClicks()
{
Visit::factory()->create();
$expected = 1;
$actual = $this->visit->totalClick();
$this->assertSame($expected, $actual);
}
/**
* Total klik untuk url yang dibuat oleh user tertentu
*
* @test
* @group u-model
*/
public function totalClicksForUrlCreatedByMe()
{
Visit::factory()->create([
'url_author_id' => $this->admin()->id,
]);
$expected = 1;
$actual = $this->visit->totalClickPerUser($this->admin()->id);
$this->assertSame($expected, $actual);
}
/**
* Total klik untuk url yang dibuat oleh Guest
*
* @test
* @group u-model
*/
public function totalClicksForUrlCreatedByGuest()
{
Visit::factory()->create([
'url_author_id' => null,
]);
$expected = 1;
$actual = $this->visit->totalClickPerUser(null);
$this->assertSame($expected, $actual);
}
/**
* @test
* @group u-model
*/
public function totalClickPerUrl()
{
$url = Visit::factory()->create([
'is_first_click' => true,
]);
Visit::factory()->create([
'url_id' => $url->url_id,
'is_first_click' => false,
]);
$expected = 2;
$actual = $this->visit->totalClickPerUrl($url->url_id);
$this->assertSame($expected, $actual);
}
/**
* @test
* @group u-model
*/
public function totalClickPerUrlAndUnique()
{
$url = Visit::factory()->create([
'is_first_click' => true,
]);
Visit::factory()->create([
'url_id' => $url->url_id,
'is_first_click' => false,
]);
$expected = 1;
$actual = $this->visit->totalClickPerUrl($url->url_id, unique: true);
$this->assertSame($expected, $actual);
}
}

@ -0,0 +1,332 @@
<?php
namespace Tests\Unit\Services;
use App\Models\Url;
use App\Services\KeyGeneratorService;
use Tests\TestCase;
class KeyGeneratorServiceTest extends TestCase
{
private Url $url;
private KeyGeneratorService $keyGeneratorService;
private const N_URL_WITH_USER_ID = 1;
private const N_URL_WITHOUT_USER_ID = 2;
private int $totalUrl;
protected function setUp(): void
{
parent::setUp();
$this->url = new Url;
$this->keyGeneratorService = app(KeyGeneratorService::class);
$this->totalUrl = self::N_URL_WITH_USER_ID + self::N_URL_WITHOUT_USER_ID;
}
/**
* String yang dihasilkan dengan memotong string url dari belakang sepanjang
* panjang karakter yang telah ditentukan.
*
* @test
* @group u-model
*/
public function urlKey_default_value()
{
$length = 3;
config(['urlhub.hash_length' => $length]);
$longUrl = 'https://github.com/realodix';
$urlKey = $this->keyGeneratorService->urlKey($longUrl);
$this->assertSame(substr($longUrl, -$length), $urlKey);
}
/**
* Karena kunci sudah ada, maka generator akan terus diulangi hingga
* menghasilkan kunci yang unik atau tidak ada yang sama.
*
* @test
* @group u-model
*/
public function urlKey_generated_string()
{
$length = 3;
config(['urlhub.hash_length' => $length]);
$longUrl = 'https://github.com/realodix';
Url::factory()->create([
'keyword' => $this->keyGeneratorService->urlKey($longUrl),
]);
$this->assertNotSame(substr($longUrl, -$length), $this->keyGeneratorService->urlKey($longUrl));
}
/**
* Panjang dari karakter kunci yang dihasilkan harus sama dengan panjang
* karakter yang telah ditentukan.
*
* @test
* @group u-model
*/
public function urlKey_specified_hash_length()
{
config(['urlhub.hash_length' => 6]);
$actual = 'https://github.com/realodix';
$expected = 'alodix';
$this->assertSame($expected, $this->keyGeneratorService->urlKey($actual));
config(['urlhub.hash_length' => 9]);
$actual = 'https://github.com/realodix';
$expected = 'mrealodix';
$this->assertSame($expected, $this->keyGeneratorService->urlKey($actual));
config(['urlhub.hash_length' => 12]);
$actual = 'https://github.com/realodix';
$expected = 'bcomrealodix';
$this->assertSame($expected, $this->keyGeneratorService->urlKey($actual));
}
/**
* Karakter yang dihasilkan harus benar-benar mengikuti karakter yang telah
* ditentukan.
*
* @test
* @group u-model
*/
public function urlKey_specified_character()
{
$url = 'https://example.com/abc';
config(['urlhub.hash_length' => 3]);
$this->assertSame('abc', $this->keyGeneratorService->urlKey($url));
config(['urlhub.hash_char' => 'xyz']);
$this->assertMatchesRegularExpression('/[xyz]/', $this->keyGeneratorService->urlKey($url));
$this->assertDoesNotMatchRegularExpression('/[abc]/', $this->keyGeneratorService->urlKey($url));
config(['urlhub.hash_length' => 4]);
config(['urlhub.hash_char' => 'abcm']);
$this->assertSame('mabc', $this->keyGeneratorService->urlKey($url));
}
/**
* String yang dihasilkan tidak boleh sama dengan string yang telah ada di
* config('urlhub.reserved_keyword')
*
* @test
* @group u-model
*/
public function urlKey_prevent_reserved_keyword()
{
$actual = 'https://example.com/css';
$expected = 'css';
config(['urlhub.reserved_keyword' => [$expected]]);
config(['urlhub.hash_length' => strlen($expected)]);
$this->assertNotSame($expected, $this->keyGeneratorService->urlKey($actual));
}
/**
* String yang dihasilkan tidak boleh sama dengan string yang telah ada di
* registered route path. Di sini, string yang dihasilkan sebagai keyword
* adalah 'admin', dimana 'admin' sudah digunakan sebagai route path.
*
* @test
* @group u-model
*/
public function urlKey_prevent_generating_strings_that_are_in_registered_route_path()
{
$actual = 'https://example.com/admin';
$expected = 'admin';
config(['urlhub.hash_length' => strlen($expected)]);
$this->assertNotSame($expected, $this->keyGeneratorService->urlKey($actual));
}
/**
* @test
* @group u-model
*/
public function generateSimpleString()
{
config(['urlhub.hash_length' => 3]);
$this->assertSame('bar', $this->keyGeneratorService->generateSimpleString('foobar'));
$this->assertSame('bar', $this->keyGeneratorService->generateSimpleString('foob/ar'));
}
/**
* @test
* @group u-model
*/
public function assertStringCanBeUsedAsKey()
{
$this->assertTrue($this->keyGeneratorService->assertStringCanBeUsedAsKey('foo'));
$this->assertFalse($this->keyGeneratorService->assertStringCanBeUsedAsKey('login'));
}
/**
* @test
* @group u-model
*/
public function maxCapacity()
{
$hashLength = config('urlhub.hash_length');
$hashCharLength = strlen(config('urlhub.hash_char'));
$maxCapacity = pow($hashCharLength, $hashLength);
$this->assertSame($maxCapacity, $this->keyGeneratorService->maxCapacity());
}
/**
* Pengujian dilakukan berdasarkan panjang karakternya.
*
* @test
* @group u-model
*/
public function usedCapacity()
{
config(['urlhub.hash_length' => config('urlhub.hash_length') + 1]);
Url::factory()->create([
'keyword' => $this->keyGeneratorService->generateRandomString(),
]);
$this->assertSame(1, $this->keyGeneratorService->usedCapacity());
Url::factory()->create([
'keyword' => str_repeat('a', config('urlhub.hash_length')),
'is_custom' => true,
]);
$this->assertSame(2, $this->keyGeneratorService->usedCapacity());
// Karena panjang karakter 'keyword' berbeda dengan dengan 'urlhub.hash_length',
// maka ini tidak ikut terhitung.
Url::factory()->create([
'keyword' => str_repeat('b', config('urlhub.hash_length') + 2),
'is_custom' => true,
]);
$this->assertSame(2, $this->keyGeneratorService->usedCapacity());
config(['urlhub.hash_length' => config('urlhub.hash_length') + 3]);
$this->assertSame(0, $this->keyGeneratorService->usedCapacity());
$this->assertSame($this->totalUrl, $this->url->totalUrl());
}
/**
* Pengujian dilakukan berdasarkan karakter yang telah ditetapkan pada
* 'urlhub.hash_char'. Jika salah satu karakter 'keyword' tidak ada di
* 'urlhub.hash_char', maka seharusnya ini tidak dapat dihitung.
*
* @test
* @group u-model
*/
public function usedCapacity2()
{
config(['urlhub.hash_length' => 3]);
config(['urlhub.hash_char' => 'foo']);
Url::factory()->create([
'keyword' => 'foo',
'is_custom' => true,
]);
$this->assertSame(1, $this->keyGeneratorService->usedCapacity());
config(['urlhub.hash_char' => 'bar']);
Url::factory()->create([
'keyword' => 'bar',
'is_custom' => true,
]);
$this->assertSame(1, $this->keyGeneratorService->usedCapacity());
// Sudah ada 2 URL yang dibuat dengan keyword 'foo' dan 'bar', maka
// seharusnya ada 2 saja.
config(['urlhub.hash_char' => 'foobar']);
$this->assertSame(2, $this->keyGeneratorService->usedCapacity());
// Sudah ada 2 URL yang dibuat dengan keyword 'foo' dan 'bar', maka
// seharusnya ada 1 saja karena 'bar' tidak bisa terhitung.
config(['urlhub.hash_char' => 'fooBar']);
$this->assertSame(1, $this->keyGeneratorService->usedCapacity());
// Sudah ada 2 URL yang dibuat dengan keyword 'foo' dan 'bar', maka
// seharusnya tidak ada sama sekali karena 'foo' dan 'bar' tidak
// bisa terhitung.
config(['urlhub.hash_char' => 'FooBar']);
$this->assertSame(0, $this->keyGeneratorService->usedCapacity());
}
/**
* @test
* @group u-model
* @dataProvider idleCapacityProvider
*
* @param mixed $kc
* @param mixed $ku
* @param mixed $expected
*/
public function idleCapacity($kc, $ku, $expected)
{
$mock = \Mockery::mock(KeyGeneratorService::class)->makePartial();
$mock->shouldReceive([
'maxCapacity' => $kc,
'usedCapacity' => $ku,
]);
$actual = $mock->idleCapacity();
$this->assertSame($expected, $actual);
}
public function idleCapacityProvider()
{
// maxCapacity(), usedCapacity(), expected_result
return [
[1, 2, 0],
[3, 2, 1],
[100, 99, 1],
[100, 20, 80],
[100, 100, 0],
];
}
/**
* @test
* @group u-model
* @dataProvider idleCapacityInPercentProvider
*
* @param mixed $kc
* @param mixed $ku
* @param mixed $expected
*/
public function idleCapacityInPercent($kc, $ku, $expected)
{
// https://ralphjsmit.com/laravel-mock-dependencies
$mock = \Mockery::mock(KeyGeneratorService::class)->makePartial();
$mock->shouldReceive([
'maxCapacity' => $kc,
'usedCapacity' => $ku,
]);
$actual = $mock->idleCapacityInPercent();
$this->assertSame($expected, $actual);
}
public function idleCapacityInPercentProvider()
{
// maxCapacity(), usedCapacity(), expected_result
return [
[10, 10, '0%'],
[10, 11, '0%'],
[pow(10, 6), 999991, '0.01%'],
[pow(10, 6), 50, '99.99%'],
[pow(10, 6), 0, '100%'],
];
}
}

@ -0,0 +1,32 @@
<?php
namespace Tests\Unit\Services;
use App\Services\UHubLinkService;
use Tests\TestCase;
class UHubLinkServiceTest extends TestCase
{
private UHubLinkService $uHubLinkService;
protected function setUp(): void
{
parent::setUp();
$this->uHubLinkService = app(UHubLinkService::class);
}
/**
* @test
*/
public function title()
{
$expected = 'example123456789.com - Untitled';
$actual = $this->uHubLinkService->title('https://example123456789.com');
$this->assertSame($expected, $actual);
$expected = 'www.example123456789.com - Untitled';
$actual = $this->uHubLinkService->title('https://www.example123456789.com');
$this->assertSame($expected, $actual);
}
}
Loading…
Cancel
Save