Page MenuHomePhorge

D1714.1775293593.diff
No OneTemporary

Authored By
Unknown
Size
46 KB
Referenced Files
None
Subscribers
None

D1714.1775293593.diff

diff --git a/src/app/Console/Commands/Sku/ListUsers.php b/src/app/Console/Commands/Sku/ListUsers.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Sku/ListUsers.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace App\Console\Commands\Sku;
+
+use Illuminate\Console\Command;
+
+class ListUsers extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'sku:list-users {sku}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'List users with the SKU entitlement.';
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $sku = \App\Sku::find($this->argument('sku'));
+
+ if (!$sku) {
+ $sku = \App\Sku::where('title', $this->argument('sku'))->first();
+ }
+
+ if (!$sku) {
+ $this->error("Unable to find the SKU.");
+ return 1;
+ }
+
+ $fn = function ($entitlement) {
+ $user_id = $entitlement->user_id;
+ if ($entitlement->entitleable_type == \App\User::class) {
+ $user_id = $entitlement->entitleable_id;
+ }
+
+ return $user_id;
+ };
+
+ $users = \App\Entitlement::select('user_id', 'entitleable_id', 'entitleable_type')
+ ->join('wallets', 'wallets.id', '=', 'wallet_id')
+ ->where('sku_id', $sku->id)
+ ->get()
+ ->map($fn)
+ ->unique();
+
+ // TODO: This wereIn() might not scale
+ \App\User::whereIn('id', $users)->orderBy('email')->get()
+ ->pluck('email')
+ ->each(function ($email, $key) {
+ $this->info($email);
+ });
+ }
+}
diff --git a/src/app/Console/Commands/UserAssignSku.php b/src/app/Console/Commands/UserAssignSku.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/UserAssignSku.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+
+class UserAssignSku extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'user:assign-sku {user} {sku} {--qty=}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Assign a SKU to the user';
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $user = \App\User::where('email', $this->argument('user'))->first();
+
+ if (!$user) {
+ $this->error("Unable to find the user {$this->argument('user')}.");
+ return 1;
+ }
+
+ $sku = \App\Sku::find($this->argument('sku'));
+
+ if (!$sku) {
+ $sku = \App\Sku::where('title', $this->argument('sku'))->first();
+ }
+
+ if (!$sku) {
+ $this->error("Unable to find the SKU {$this->argument('sku')}.");
+ return 1;
+ }
+
+ $quantity = (int) $this->option('qty');
+
+ // Check if the entitlement already exists
+ if (empty($quantity)) {
+ if ($user->entitlements()->where('sku_id', $sku->id)->first()) {
+ $this->error("The entitlement already exists. Maybe try with --qty=X?");
+ return 1;
+ }
+ }
+
+ $user->assignSku($sku, $quantity ?: 1);
+ }
+}
diff --git a/src/app/Handlers/Activesync.php b/src/app/Handlers/Activesync.php
--- a/src/app/Handlers/Activesync.php
+++ b/src/app/Handlers/Activesync.php
@@ -4,21 +4,38 @@
class Activesync extends \App\Handlers\Base
{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
return \App\User::class;
}
- public static function preReq($entitlement, $object): bool
+ /**
+ * SKU handler metadata.
+ *
+ * @param \App\Sku $sku The SKU object
+ *
+ * @return array
+ */
+ public static function metadata(\App\Sku $sku): array
{
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
+ $data = parent::metadata($sku);
- return true;
+ $data['required'] = ['groupware'];
+
+ return $data;
}
+ /**
+ * The priority that specifies the order of SKUs in UI.
+ * Higher number means higher on the list.
+ *
+ * @return int
+ */
public static function priority(): int
{
return 70;
diff --git a/src/app/Handlers/Auth2F.php b/src/app/Handlers/Auth2F.php
--- a/src/app/Handlers/Auth2F.php
+++ b/src/app/Handlers/Auth2F.php
@@ -4,21 +4,38 @@
class Auth2F extends \App\Handlers\Base
{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
return \App\User::class;
}
- public static function preReq($entitlement, $object): bool
+ /**
+ * SKU handler metadata.
+ *
+ * @param \App\Sku $sku The SKU object
+ *
+ * @return array
+ */
+ public static function metadata(\App\Sku $sku): array
{
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
+ $data = parent::metadata($sku);
- return true;
+ $data['forbidden'] = ['activesync'];
+
+ return $data;
}
+ /**
+ * The priority that specifies the order of SKUs in UI.
+ * Higher number means higher on the list.
+ *
+ * @return int
+ */
public static function priority(): int
{
return 60;
diff --git a/src/app/Handlers/Base.php b/src/app/Handlers/Base.php
--- a/src/app/Handlers/Base.php
+++ b/src/app/Handlers/Base.php
@@ -15,6 +15,56 @@
}
/**
+ * Check if the SKU is available to the user. An SKU is available
+ * to the user when either it is active or there's already an
+ * active entitlement.
+ *
+ * @param \App\Sku $sku The SKU object
+ * @param \App\User $user The user object
+ *
+ * @return bool
+ */
+ public static function isAvailable(\App\Sku $sku, \App\User $user): bool
+ {
+ if (!$sku->active) {
+ if (!$user->entitlements()->where('sku_id', $sku->id)->first()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Metadata of this SKU handler.
+ *
+ * @param \App\Sku $sku The SKU object
+ *
+ * @return array
+ */
+ public static function metadata(\App\Sku $sku): array
+ {
+ $handler = explode('\\', static::class);
+ $handler = strtolower(end($handler));
+
+ $type = explode('\\', static::entitleableClass());
+ $type = strtolower(end($type));
+
+ return [
+ // entitleable type
+ 'type' => $type,
+ // handler (as a keyword)
+ 'handler' => $handler,
+ // readonly entitlement state cannot be changed
+ 'readonly' => false,
+ // is entitlement enabled by default?
+ 'enabled' => false,
+ // priority on the entitlements list
+ 'prio' => static::priority(),
+ ];
+ }
+
+ /**
* Prerequisites for the Entitlement to be applied to the object.
*
* @param \App\Entitlement $entitlement
@@ -24,6 +74,23 @@
*/
public static function preReq($entitlement, $object): bool
{
+ $type = static::entitleableClass();
+
+ if (empty($type) || empty($entitlement->entitleable_type)) {
+ \Log::error("Entitleable class/type not specified");
+ return false;
+ }
+
+ if ($type !== $entitlement->entitleable_type) {
+ \Log::error("Entitleable class mismatch");
+ return false;
+ }
+
+ if (!$entitlement->sku->active) {
+ \Log::error("Sku not active");
+ return false;
+ }
+
return true;
}
diff --git a/src/app/Handlers/Base.php b/src/app/Handlers/Beta.php
copy from src/app/Handlers/Base.php
copy to src/app/Handlers/Beta.php
--- a/src/app/Handlers/Base.php
+++ b/src/app/Handlers/Beta.php
@@ -2,7 +2,7 @@
namespace App\Handlers;
-abstract class Base
+class Beta extends \App\Handlers\Base
{
/**
* The entitleable class for this handler.
@@ -11,7 +11,7 @@
*/
public static function entitleableClass(): string
{
- return '';
+ return \App\User::class;
}
/**
@@ -24,6 +24,13 @@
*/
public static function preReq($entitlement, $object): bool
{
+ // We allow inactive "beta" Sku to be assigned
+
+ if (self::entitleableClass() !== $entitlement->entitleable_type) {
+ \Log::error("Entitleable class mismatch");
+ return false;
+ }
+
return true;
}
@@ -35,6 +42,7 @@
*/
public static function priority(): int
{
- return 0;
+ // Just above all other beta SKUs, please
+ return 10;
}
}
diff --git a/src/app/Handlers/Beta/Base.php b/src/app/Handlers/Beta/Base.php
new file mode 100644
--- /dev/null
+++ b/src/app/Handlers/Beta/Base.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace App\Handlers\Beta;
+
+class Base extends \App\Handlers\Base
+{
+ /**
+ * Check if the SKU is available to the user.
+ *
+ * @param \App\Sku $sku The SKU object
+ * @param \App\User $user The user object
+ *
+ * @return bool
+ */
+ public static function isAvailable(\App\Sku $sku, \App\User $user): bool
+ {
+ // These SKUs must be:
+ // 1) already assigned or
+ // 2) active and a 'beta' entitlement must exist.
+
+ if ($sku->active) {
+ $beta = \App\Sku::where('title', 'beta')->first();
+ if (!$beta) {
+ return false;
+ }
+
+ if ($user->entitlements()->where('sku_id', $beta->id)->first()) {
+ return true;
+ }
+ } else {
+ if ($user->entitlements()->where('sku_id', $sku->id)->first()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * SKU handler metadata.
+ *
+ * @param \App\Sku $sku The SKU object
+ *
+ * @return array
+ */
+ public static function metadata(\App\Sku $sku): array
+ {
+ $data = parent::metadata($sku);
+
+ $data['required'] = ['beta'];
+
+ return $data;
+ }
+
+ /**
+ * Prerequisites for the Entitlement to be applied to the object.
+ *
+ * @param \App\Entitlement $entitlement
+ * @param mixed $object
+ *
+ * @return bool
+ */
+ public static function preReq($entitlement, $object): bool
+ {
+ if (!parent::preReq($entitlement, $object)) {
+ return false;
+ }
+
+ // TODO: User has to have the "beta" entitlement
+
+ return true;
+ }
+}
diff --git a/src/app/Handlers/Beta/Meet.php b/src/app/Handlers/Beta/Meet.php
new file mode 100644
--- /dev/null
+++ b/src/app/Handlers/Beta/Meet.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace App\Handlers\Beta;
+
+class Meet extends Base
+{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
+ public static function entitleableClass(): string
+ {
+ // Note: We connot just inherit from the parent because
+ // we use static:: there.
+ return \App\User::class;
+ }
+}
diff --git a/src/app/Handlers/Domain.php b/src/app/Handlers/Domain.php
--- a/src/app/Handlers/Domain.php
+++ b/src/app/Handlers/Domain.php
@@ -4,18 +4,13 @@
class Domain extends \App\Handlers\Base
{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
return \App\Domain::class;
}
-
- public static function preReq($entitlement, $domain): bool
- {
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
-
- return true;
- }
}
diff --git a/src/app/Handlers/DomainHosting.php b/src/app/Handlers/DomainHosting.php
--- a/src/app/Handlers/DomainHosting.php
+++ b/src/app/Handlers/DomainHosting.php
@@ -4,18 +4,13 @@
class DomainHosting extends \App\Handlers\Base
{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
return \App\Domain::class;
}
-
- public static function preReq($entitlement, $domain): bool
- {
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
-
- return true;
- }
}
diff --git a/src/app/Handlers/DomainRegistration.php b/src/app/Handlers/DomainRegistration.php
--- a/src/app/Handlers/DomainRegistration.php
+++ b/src/app/Handlers/DomainRegistration.php
@@ -4,18 +4,13 @@
class DomainRegistration extends \App\Handlers\Base
{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
return \App\Domain::class;
}
-
- public static function preReq($entitlement, $domain): bool
- {
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
-
- return true;
- }
}
diff --git a/src/app/Handlers/DomainRelay.php b/src/app/Handlers/DomainRelay.php
--- a/src/app/Handlers/DomainRelay.php
+++ b/src/app/Handlers/DomainRelay.php
@@ -4,18 +4,13 @@
class DomainRelay extends \App\Handlers\Base
{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
return \App\Domain::class;
}
-
- public static function preReq($entitlement, $domain): bool
- {
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
-
- return true;
- }
}
diff --git a/src/app/Handlers/Groupware.php b/src/app/Handlers/Groupware.php
--- a/src/app/Handlers/Groupware.php
+++ b/src/app/Handlers/Groupware.php
@@ -4,21 +4,16 @@
class Groupware extends \App\Handlers\Base
{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
return \App\User::class;
}
- public static function preReq($entitlement, $user): bool
- {
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
-
- return true;
- }
-
/**
* The priority that specifies the order of SKUs in UI.
* Higher number means higher on the list.
diff --git a/src/app/Handlers/Mailbox.php b/src/app/Handlers/Mailbox.php
--- a/src/app/Handlers/Mailbox.php
+++ b/src/app/Handlers/Mailbox.php
@@ -4,37 +4,32 @@
class Mailbox extends \App\Handlers\Base
{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
return \App\User::class;
}
- public static function preReq($entitlement, $user): bool
+ /**
+ * SKU handler metadata.
+ *
+ * @param \App\Sku $sku The SKU object
+ *
+ * @return array
+ */
+ public static function metadata(\App\Sku $sku): array
{
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
-/*
- FIXME: This code prevents from creating initial mailbox SKU
- on signup of group account, because User::domains()
- does not return the new domain.
- Either we make sure to create domain entitlement before mailbox
- entitlement or make the method here aware of that case or?
-
- list($local, $domain) = explode('@', $user->email);
-
- $domains = $user->domains();
+ $data = parent::metadata($sku);
- foreach ($domains as $_domain) {
- if ($domain == $_domain->namespace) {
- return true;
- }
- }
+ // Mailbox is always enabled and cannot be unset
+ $data['readonly'] = true;
+ $data['enabled'] = true;
- \Log::info("Domain not for user");
-*/
- return true;
+ return $data;
}
/**
diff --git a/src/app/Handlers/Resource.php b/src/app/Handlers/Resource.php
--- a/src/app/Handlers/Resource.php
+++ b/src/app/Handlers/Resource.php
@@ -4,19 +4,14 @@
class Resource extends \App\Handlers\Base
{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
// TODO
return '';
}
-
- public static function preReq($entitlement, $owner): bool
- {
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
-
- return true;
- }
}
diff --git a/src/app/Handlers/SharedFolder.php b/src/app/Handlers/SharedFolder.php
--- a/src/app/Handlers/SharedFolder.php
+++ b/src/app/Handlers/SharedFolder.php
@@ -4,19 +4,14 @@
class SharedFolder extends \App\Handlers\Base
{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
// TODO
return '';
}
-
- public static function preReq($entitlement, $owner): bool
- {
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
-
- return true;
- }
}
diff --git a/src/app/Handlers/Storage.php b/src/app/Handlers/Storage.php
--- a/src/app/Handlers/Storage.php
+++ b/src/app/Handlers/Storage.php
@@ -7,21 +7,36 @@
public const MAX_ITEMS = 100;
public const ITEM_UNIT = 'GB';
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
return \App\User::class;
}
- public static function preReq($entitlement, $object): bool
+ /**
+ * SKU handler metadata.
+ *
+ * @param \App\Sku $sku The SKU object
+ *
+ * @return array
+ */
+ public static function metadata(\App\Sku $sku): array
{
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
+ $data = parent::metadata($sku);
- // TODO: The storage can not be modified to below what is already consumed.
+ $data['readonly'] = true; // only the checkbox will be disabled, not range
+ $data['enabled'] = true;
+ $data['range'] = [
+ 'min' => $sku->units_free,
+ 'max' => self::MAX_ITEMS,
+ 'unit' => self::ITEM_UNIT,
+ ];
- return true;
+ return $data;
}
/**
diff --git a/src/app/Http/Controllers/API/V4/SkusController.php b/src/app/Http/Controllers/API/V4/SkusController.php
--- a/src/app/Http/Controllers/API/V4/SkusController.php
+++ b/src/app/Http/Controllers/API/V4/SkusController.php
@@ -5,6 +5,7 @@
use App\Http\Controllers\Controller;
use App\Sku;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
class SkusController extends Controller
{
@@ -46,18 +47,14 @@
}
/**
- * Display a listing of the sku.
+ * Get a list of active SKUs.
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
// Note: Order by title for consistent ordering in tests
- $skus = Sku::select()->orderBy('title')->get();
-
- // Note: we do not limit the result to active SKUs only.
- // It's because we might need users assigned to old SKUs,
- // we need to display these old SKUs on the entitlements list
+ $skus = Sku::where('active', true)->orderBy('title')->get();
$response = [];
@@ -115,6 +112,52 @@
}
/**
+ * Get a list of SKUs available to the user.
+ *
+ * @param int $id User identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function userSkus($id)
+ {
+ $user = \App\User::find($id);
+
+ if (empty($user)) {
+ return $this->errorResponse(404);
+ }
+
+ if (!Auth::guard()->user()->canRead($user)) {
+ return $this->errorResponse(403);
+ }
+
+ $type = request()->input('type');
+ $response = [];
+
+ // Note: Order by title for consistent ordering in tests
+ $skus = Sku::orderBy('title')->get();
+
+ foreach ($skus as $sku) {
+ if (!$sku->handler_class::isAvailable($sku, $user)) {
+ continue;
+ }
+
+ if ($data = $this->skuElement($sku)) {
+ if ($type && $type != $data['type']) {
+ continue;
+ }
+
+ $response[] = $data;
+ }
+ }
+
+ usort($response, function ($a, $b) {
+ return ($b['prio'] <=> $a['prio']);
+ });
+
+ return response()->json($response);
+ }
+
+ /**
* Convert SKU information to metadata used by UI to
* display the form control
*
@@ -124,59 +167,18 @@
*/
protected function skuElement($sku): ?array
{
- $type = $sku->handler_class::entitleableClass();
+ $data = array_merge($sku->toArray(), $sku->handler_class::metadata($sku));
// ignore incomplete handlers
- if (!$type) {
+ if (empty($data['type'])) {
return null;
}
- $type = explode('\\', $type);
- $type = strtolower(end($type));
-
- $handler = explode('\\', $sku->handler_class);
- $handler = strtolower(end($handler));
-
- $data = $sku->toArray();
-
- $data['type'] = $type;
- $data['handler'] = $handler;
- $data['readonly'] = false;
- $data['enabled'] = false;
- $data['prio'] = $sku->handler_class::priority();
-
// Use localized value, toArray() does not get them right
$data['name'] = $sku->name;
$data['description'] = $sku->description;
- unset($data['handler_class']);
-
- switch ($handler) {
- case 'activesync':
- $data['required'] = ['groupware'];
- break;
-
- case 'auth2f':
- $data['forbidden'] = ['activesync'];
- break;
-
- case 'storage':
- // Quota range input
- $data['readonly'] = true; // only the checkbox will be disabled, not range
- $data['enabled'] = true;
- $data['range'] = [
- 'min' => $data['units_free'],
- 'max' => $sku->handler_class::MAX_ITEMS,
- 'unit' => $sku->handler_class::ITEM_UNIT,
- ];
- break;
-
- case 'mailbox':
- // Mailbox is always enabled and cannot be unset
- $data['readonly'] = true;
- $data['enabled'] = true;
- break;
- }
+ unset($data['handler_class'], $data['created_at'], $data['updated_at']);
return $data;
}
diff --git a/src/app/Observers/EntitlementObserver.php b/src/app/Observers/EntitlementObserver.php
--- a/src/app/Observers/EntitlementObserver.php
+++ b/src/app/Observers/EntitlementObserver.php
@@ -24,14 +24,6 @@
*/
public function creating(Entitlement $entitlement): bool
{
- while (true) {
- $allegedly_unique = \App\Utils::uuidStr();
- if (!Entitlement::find($allegedly_unique)) {
- $entitlement->{$entitlement->getKeyName()} = $allegedly_unique;
- break;
- }
- }
-
// can't dispatch job here because it'll fail serialization
// Make sure the owner is at least a controller on the wallet
@@ -53,6 +45,14 @@
return false;
}
+ while (true) {
+ $allegedly_unique = \App\Utils::uuidStr();
+ if (!Entitlement::find($allegedly_unique)) {
+ $entitlement->{$entitlement->getKeyName()} = $allegedly_unique;
+ break;
+ }
+ }
+
return true;
}
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -175,12 +175,6 @@
$wallet = $this->wallet();
$exists = $this->entitlements()->where('sku_id', $sku->id)->count();
- // TODO: Sanity check, this probably should be in preReq() on handlers
- // or in EntitlementObserver
- if ($sku->handler_class::entitleableClass() != User::class) {
- throw new \Exception("Cannot assign non-user SKU ({$sku->title}) to a user");
- }
-
while ($count > 0) {
\App\Entitlement::create([
'wallet_id' => $wallet->id,
diff --git a/src/database/seeds/local/SkuSeeder.php b/src/database/seeds/local/SkuSeeder.php
--- a/src/database/seeds/local/SkuSeeder.php
+++ b/src/database/seeds/local/SkuSeeder.php
@@ -151,5 +151,31 @@
'active' => true,
]
);
+
+ Sku::create(
+ [
+ 'title' => 'beta',
+ 'name' => 'Beta program',
+ 'description' => 'Access to beta program subscriptions',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Beta',
+ 'active' => false,
+ ]
+ );
+
+ Sku::create(
+ [
+ 'title' => 'meet',
+ 'name' => 'Video chat',
+ 'description' => 'Video conferencing tool',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Beta\Meet',
+ 'active' => true,
+ ]
+ );
}
}
diff --git a/src/database/seeds/production/SkuSeeder.php b/src/database/seeds/production/SkuSeeder.php
--- a/src/database/seeds/production/SkuSeeder.php
+++ b/src/database/seeds/production/SkuSeeder.php
@@ -151,5 +151,31 @@
'active' => true,
]
);
+
+ Sku::create(
+ [
+ 'title' => 'beta',
+ 'name' => 'Beta program',
+ 'description' => 'Access to beta program subscriptions',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Beta',
+ 'active' => false,
+ ]
+ );
+
+ Sku::create(
+ [
+ 'title' => 'meet',
+ 'name' => 'Video chat',
+ 'description' => 'Video conferencing tool',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Beta\Meet',
+ 'active' => true,
+ ]
+ );
}
}
diff --git a/src/resources/vue/Admin/User.vue b/src/resources/vue/Admin/User.vue
--- a/src/resources/vue/Admin/User.vue
+++ b/src/resources/vue/Admin/User.vue
@@ -450,7 +450,7 @@
})
// Create subscriptions list
- axios.get('/api/v4/skus')
+ axios.get('/api/v4/users/' + user_id + '/skus')
.then(response => {
// "merge" SKUs with user entitlement-SKUs
response.data.forEach(sku => {
@@ -470,7 +470,7 @@
this.skus.push(item)
- if (sku.title == '2fa') {
+ if (sku.handler == 'auth2f') {
this.has2FA = true
this.sku2FA = sku.id
}
diff --git a/src/resources/vue/User/Info.vue b/src/resources/vue/User/Info.vue
--- a/src/resources/vue/User/Info.vue
+++ b/src/resources/vue/User/Info.vue
@@ -219,11 +219,10 @@
this.discount_description = this.user.wallet.discount_description
this.status = response.data.statusInfo
- axios.get('/api/v4/skus')
+ axios.get('/api/v4/users/' + this.user_id + '/skus?type=user')
.then(response => {
// "merge" SKUs with user entitlement-SKUs
this.skus = response.data
- .filter(sku => sku.type == 'user')
.map(sku => {
if (sku.id in this.user.skus) {
sku.enabled = true
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -67,6 +67,7 @@
Route::apiResource('packages', API\V4\PackagesController::class);
Route::apiResource('skus', API\V4\SkusController::class);
Route::apiResource('users', API\V4\UsersController::class);
+ Route::get('users/{id}/skus', 'API\V4\SkusController@userSkus');
Route::get('users/{id}/status', 'API\V4\UsersController@status');
Route::apiResource('wallets', API\V4\WalletsController::class);
@@ -120,6 +121,7 @@
Route::apiResource('skus', API\V4\Admin\SkusController::class);
Route::apiResource('users', API\V4\Admin\UsersController::class);
Route::post('users/{id}/reset2FA', 'API\V4\Admin\UsersController@reset2FA');
+ Route::get('users/{id}/skus', 'API\V4\Admin\SkusController@userSkus');
Route::post('users/{id}/suspend', 'API\V4\Admin\UsersController@suspend');
Route::post('users/{id}/unsuspend', 'API\V4\Admin\UsersController@unsuspend');
Route::apiResource('wallets', API\V4\Admin\WalletsController::class);
diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php
--- a/src/tests/Browser/UsersTest.php
+++ b/src/tests/Browser/UsersTest.php
@@ -46,6 +46,9 @@
$wallet = $john->wallets()->first();
$wallet->discount()->dissociate();
$wallet->save();
+
+ $betas = Sku::where('handler_class', 'like', '%\\Beta%')->pluck('id')->all();
+ Entitlement::whereIn('sku_id', $betas)->delete();
}
/**
@@ -66,6 +69,9 @@
$wallet->discount()->dissociate();
$wallet->save();
+ $betas = Sku::where('handler_class', 'like', '%\\Beta%')->pluck('id')->all();
+ Entitlement::whereIn('sku_id', $betas)->delete();
+
parent::tearDown();
}
@@ -583,4 +589,68 @@
});
});
}
+
+ /**
+ * Test beta entitlements
+ *
+ * @depends testList
+ */
+ public function testBetaEntitlements(): void
+ {
+ $this->browse(function (Browser $browser) {
+ $john = User::where('email', 'john@kolab.org')->first();
+ $sku = Sku::where('title', 'beta')->first();
+ $john->assignSku($sku);
+
+ $browser->visit('/user/' . $john->id)
+ ->on(new UserInfo())
+ ->with('@skus', function ($browser) {
+ $browser->assertElementsCount('tbody tr', 7)
+ // Beta SKU
+ ->assertSeeIn('tbody tr:nth-child(6) td.name', 'Beta program')
+ ->assertSeeIn('tbody tr:nth-child(6) td.price', '0,00 CHF/month')
+ ->assertChecked('tbody tr:nth-child(6) td.selection input')
+ ->assertEnabled('tbody tr:nth-child(6) td.selection input')
+ ->assertTip(
+ 'tbody tr:nth-child(6) td.buttons button',
+ 'Access to beta program subscriptions'
+ )
+ // Beta/Meet SKU
+ ->assertSeeIn('tbody tr:nth-child(7) td.name', 'Video chat')
+ ->assertSeeIn('tr:nth-child(7) td.price', '0,00 CHF/month')
+ ->assertNotChecked('tbody tr:nth-child(7) td.selection input')
+ ->assertEnabled('tbody tr:nth-child(7) td.selection input')
+ ->assertTip(
+ 'tbody tr:nth-child(7) td.buttons button',
+ 'Video conferencing tool'
+ )
+ // Check Meet, Uncheck Beta, expect Meet unchecked
+ ->click('#sku-input-meet')
+ ->click('#sku-input-beta')
+ ->assertNotChecked('#sku-input-beta')
+ ->assertNotChecked('#sku-input-meet')
+ // Click Meet expect an alert
+ ->click('#sku-input-meet')
+ ->assertDialogOpened('Video chat requires Beta program.')
+ ->acceptDialog()
+ // Enable Meet and Beta and submit
+ ->click('#sku-input-beta')
+ ->click('#sku-input-meet');
+ })
+ ->click('button[type=submit]')
+ ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
+
+ $expected = ['beta', 'groupware', 'mailbox', 'meet', 'storage', 'storage'];
+ $this->assertUserEntitlements($john, $expected);
+
+ $browser->visit('/user/' . $john->id)
+ ->on(new UserInfo())
+ ->click('#sku-input-beta')
+ ->click('button[type=submit]')
+ ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
+
+ $expected = ['groupware', 'mailbox', 'storage', 'storage'];
+ $this->assertUserEntitlements($john, $expected);
+ });
+ }
}
diff --git a/src/tests/Feature/Console/Sku/ListUsersTest.php b/src/tests/Feature/Console/Sku/ListUsersTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/Sku/ListUsersTest.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Tests\Feature\Console\Sku;
+
+use Illuminate\Contracts\Console\Kernel;
+use Tests\TestCase;
+
+class ListUsersTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('sku-list-users@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('sku-list-users@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test command runs
+ */
+ public function testHandle(): void
+ {
+ // Warning: We're not using artisan() here, as this will not
+ // allow us to test "empty output" cases
+ $code = \Artisan::call('sku:list-users meet');
+ $output = trim(\Artisan::output());
+ $this->assertSame(0, $code);
+ $this->assertSame('', $output);
+
+ $code = \Artisan::call('sku:list-users unknown');
+ $output = trim(\Artisan::output());
+ $this->assertSame(1, $code);
+ $this->assertSame("Unable to find the SKU.", $output);
+
+ $code = \Artisan::call('sku:list-users 2fa');
+ $output = trim(\Artisan::output());
+ $this->assertSame(0, $code);
+ $this->assertSame("ned@kolab.org", $output);
+
+ $code = \Artisan::call('sku:list-users mailbox');
+ $output = trim(\Artisan::output());
+ $this->assertSame(0, $code);
+ $this->assertSame("jack@kolab.org\njoe@kolab.org\njohn@kolab.org\nned@kolab.org", $output);
+
+ $code = \Artisan::call('sku:list-users domain-hosting');
+ $output = trim(\Artisan::output());
+ $this->assertSame(0, $code);
+ $this->assertSame("john@kolab.org", $output);
+
+ $sku = \App\Sku::where('title', 'meet')->first();
+ $user = $this->getTestUser('sku-list-users@kolabnow.com');
+ $user->assignSku($sku);
+
+ $code = \Artisan::call('sku:list-users meet');
+ $output = trim(\Artisan::output());
+ $this->assertSame(0, $code);
+ $this->assertSame($user->email, $output);
+
+ $user->assignSku($sku);
+
+ $code = \Artisan::call('sku:list-users meet');
+ $output = trim(\Artisan::output());
+ $this->assertSame(0, $code);
+ $this->assertSame($user->email, $output);
+ }
+}
diff --git a/src/tests/Feature/Console/UserAssignSkuTest.php b/src/tests/Feature/Console/UserAssignSkuTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/UserAssignSkuTest.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Tests\Feature\Console;
+
+use Tests\TestCase;
+
+class UserAssignSkuTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('add-entitlement@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('add-entitlement@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test command runs
+ */
+ public function testHandle(): void
+ {
+ $sku = \App\Sku::where('title', 'meet')->first();
+ $user = $this->getTestUser('add-entitlement@kolabnow.com');
+
+ $this->artisan('user:assign-sku unknown@unknown.org ' . $sku->id)
+ ->assertExitCode(1)
+ ->expectsOutput("Unable to find the user unknown@unknown.org.");
+
+ $this->artisan('user:assign-sku ' . $user->email . ' unknownsku')
+ ->assertExitCode(1)
+ ->expectsOutput("Unable to find the SKU unknownsku.");
+
+ $this->artisan('user:assign-sku ' . $user->email . ' ' . $sku->id)
+ ->assertExitCode(0);
+
+ $this->assertCount(1, $user->entitlements()->where('sku_id', $sku->id)->get());
+
+ // Try again (also test sku by title)
+ $this->artisan('user:assign-sku ' . $user->email . ' ' . $sku->title)
+ ->assertExitCode(1)
+ ->expectsOutput("The entitlement already exists. Maybe try with --qty=X?");
+
+ $this->assertCount(1, $user->entitlements()->where('sku_id', $sku->id)->get());
+
+ // Try again with --qty option, to force the assignment
+ $this->artisan('user:assign-sku ' . $user->email . ' ' . $sku->title . ' --qty=1')
+ ->assertExitCode(0);
+
+ $this->assertCount(2, $user->entitlements()->where('sku_id', $sku->id)->get());
+ }
+}
diff --git a/src/tests/Feature/Controller/SkusTest.php b/src/tests/Feature/Controller/SkusTest.php
--- a/src/tests/Feature/Controller/SkusTest.php
+++ b/src/tests/Feature/Controller/SkusTest.php
@@ -2,6 +2,7 @@
namespace Tests\Feature\Controller;
+use App\Entitlement;
use App\Http\Controllers\API\V4\SkusController;
use App\Sku;
use Tests\TestCase;
@@ -9,6 +10,29 @@
class SkusTest extends TestCase
{
/**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $betas = Sku::where('handler_class', 'like', '%\\Beta%')->pluck('id')->all();
+ Entitlement::whereIn('sku_id', $betas)->delete();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $betas = Sku::where('handler_class', 'like', '%\\Beta%')->pluck('id')->all();
+ Entitlement::whereIn('sku_id', $betas)->delete();
+
+ parent::tearDown();
+ }
+
+
+ /**
* Test fetching SKUs list
*/
public function testIndex(): void
@@ -25,7 +49,7 @@
$json = $response->json();
- $this->assertCount(9, $json);
+ $this->assertCount(7, $json);
$this->assertSame(100, $json[0]['prio']);
$this->assertSame($sku->id, $json[0]['id']);
@@ -41,12 +65,125 @@
}
/**
- * Test for SkusController::skuElement()
+ * Test fetching SKUs list for a user (GET /users/<id>/skus)
*/
- public function testSkuElement(): void
+ public function testUserSkus(): void
{
- $sku = Sku::where('title', 'storage')->first();
- $result = $this->invokeMethod(new SkusController(), 'skuElement', [$sku]);
+ $user = $this->getTestUser('john@kolab.org');
+
+ // Unauth access not allowed
+ $response = $this->get("api/v4/users/{$user->id}/skus");
+ $response->assertStatus(401);
+
+ $response = $this->actingAs($user)->get("api/v4/users/{$user->id}/skus");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(6, $json);
+
+ $this->assertSkuElement('mailbox', $json[0], [
+ 'prio' => 100,
+ 'type' => 'user',
+ 'handler' => 'mailbox',
+ 'enabled' => true,
+ 'readonly' => true,
+ ]);
+
+ $this->assertSkuElement('storage', $json[1], [
+ 'prio' => 90,
+ 'type' => 'user',
+ 'handler' => 'storage',
+ 'enabled' => true,
+ 'readonly' => true,
+ 'range' => [
+ 'min' => 2,
+ 'max' => 100,
+ 'unit' => 'GB',
+ ]
+ ]);
+
+ $this->assertSkuElement('groupware', $json[2], [
+ 'prio' => 80,
+ 'type' => 'user',
+ 'handler' => 'groupware',
+ 'enabled' => false,
+ 'readonly' => false,
+ ]);
+
+ $this->assertSkuElement('activesync', $json[3], [
+ 'prio' => 70,
+ 'type' => 'user',
+ 'handler' => 'activesync',
+ 'enabled' => false,
+ 'readonly' => false,
+ 'required' => ['groupware'],
+ ]);
+
+ $this->assertSkuElement('2fa', $json[4], [
+ 'prio' => 60,
+ 'type' => 'user',
+ 'handler' => 'auth2f',
+ 'enabled' => false,
+ 'readonly' => false,
+ 'forbidden' => ['activesync'],
+ ]);
+
+ $this->assertSkuElement('domain-hosting', $json[5], [
+ 'prio' => 0,
+ 'type' => 'domain',
+ 'handler' => 'domainhosting',
+ 'enabled' => false,
+ 'readonly' => false,
+ ]);
+
+ // Test filter by type
+ $response = $this->actingAs($user)->get("api/v4/users/{$user->id}/skus?type=domain");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(1, $json);
+ $this->assertSame('domain', $json[0]['type']);
+
+ // Test inclusion of beta SKUs
+ $sku = Sku::where('title', 'beta')->first();
+ $user->assignSku($sku);
+ $response = $this->actingAs($user)->get("api/v4/users/{$user->id}/skus?type=user");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(7, $json);
+
+ $this->assertSkuElement('beta', $json[5], [
+ 'prio' => 10,
+ 'type' => 'user',
+ 'handler' => 'beta',
+ 'enabled' => false,
+ 'readonly' => false,
+ ]);
+
+ $this->assertSkuElement('meet', $json[6], [
+ 'prio' => 0,
+ 'type' => 'user',
+ 'handler' => 'meet',
+ 'enabled' => false,
+ 'readonly' => false,
+ 'required' => ['beta'],
+ ]);
+ }
+
+ /**
+ * Assert content of the SKU element in an API response
+ *
+ * @param string $sku_title The SKU title
+ * @param array $result The result to assert
+ * @param array $other Other items the SKU itself does not include
+ */
+ protected function assertSkuElement($sku_title, $result, $other = []): void
+ {
+ $sku = Sku::where('title', $sku_title)->first();
$this->assertSame($sku->id, $result['id']);
$this->assertSame($sku->title, $result['title']);
@@ -56,15 +193,11 @@
$this->assertSame($sku->units_free, $result['units_free']);
$this->assertSame($sku->period, $result['period']);
$this->assertSame($sku->active, $result['active']);
- $this->assertSame('user', $result['type']);
- $this->assertSame('storage', $result['handler']);
- $this->assertSame($sku->units_free, $result['range']['min']);
- $this->assertSame($sku->handler_class::MAX_ITEMS, $result['range']['max']);
- $this->assertSame($sku->handler_class::ITEM_UNIT, $result['range']['unit']);
- $this->assertTrue($result['readonly']);
- $this->assertTrue($result['enabled']);
-
- // Test all SKU types
- $this->markTestIncomplete();
+
+ foreach ($other as $key => $value) {
+ $this->assertSame($value, $result[$key]);
+ }
+
+ $this->assertCount(8 + count($other), $result);
}
}
diff --git a/src/tests/Feature/DomainTest.php b/src/tests/Feature/DomainTest.php
--- a/src/tests/Feature/DomainTest.php
+++ b/src/tests/Feature/DomainTest.php
@@ -6,8 +6,6 @@
use App\Entitlement;
use App\Sku;
use App\User;
-use Illuminate\Foundation\Testing\WithFaker;
-use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
diff --git a/src/tests/Feature/SignupCodeTest.php b/src/tests/Feature/SignupCodeTest.php
--- a/src/tests/Feature/SignupCodeTest.php
+++ b/src/tests/Feature/SignupCodeTest.php
@@ -5,8 +5,6 @@
use App\SignupCode;
use Carbon\Carbon;
use Tests\TestCase;
-use Illuminate\Foundation\Testing\WithFaker;
-use Illuminate\Foundation\Testing\RefreshDatabase;
class SignupCodeTest extends TestCase
{
diff --git a/src/tests/Feature/VerificationCodeTest.php b/src/tests/Feature/VerificationCodeTest.php
--- a/src/tests/Feature/VerificationCodeTest.php
+++ b/src/tests/Feature/VerificationCodeTest.php
@@ -6,8 +6,6 @@
use App\VerificationCode;
use Carbon\Carbon;
use Tests\TestCase;
-use Illuminate\Foundation\Testing\WithFaker;
-use Illuminate\Foundation\Testing\RefreshDatabase;
class VerificationCodeTest extends TestCase
{
diff --git a/src/tests/Feature/WalletTest.php b/src/tests/Feature/WalletTest.php
--- a/src/tests/Feature/WalletTest.php
+++ b/src/tests/Feature/WalletTest.php
@@ -8,8 +8,6 @@
use App\Wallet;
use Carbon\Carbon;
use Tests\TestCase;
-use Illuminate\Foundation\Testing\WithFaker;
-use Illuminate\Foundation\Testing\RefreshDatabase;
class WalletTest extends TestCase
{

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 4, 9:06 AM (12 h, 1 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18828733
Default Alt Text
D1714.1775293593.diff (46 KB)

Event Timeline