Page MenuHomePhorge

D5679.1775173425.diff
No OneTemporary

Authored By
Unknown
Size
6 KB
Referenced Files
None
Subscribers
None

D5679.1775173425.diff

diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -12,6 +12,7 @@
use App\Jobs\Mail\EmailVerificationJob;
use App\Jobs\User\CreateJob;
use App\Package;
+use App\Plan;
use App\Resource;
use App\Rules\Password;
use App\Rules\UserEmailLocal;
@@ -373,6 +374,7 @@
#[BodyParameter('passwordLinkCode', description: 'Code for a by-link password reset', type: 'string')]
#[BodyParameter('skus', description: 'Enabled SKUs', type: 'array')]
#[BodyParameter('aliases', description: 'Email address aliases', type: 'array<string>')]
+ #[BodyParameter('plan_id', description: 'Plan identifier', type: 'string')]
public function update(Request $request, $id): JsonResponse
{
$user = User::find($id);
@@ -510,6 +512,17 @@
}
}
+ if (!empty($user) && !empty($request->plan_id)) {
+ $rules['plan_id'] = [
+ 'string',
+ function (string $attribute, mixed $value, \Closure $fail) use ($user) {
+ if (!$this->validatePlan($user, $value)) {
+ $fail(self::trans('validation.invalidvalue'));
+ }
+ },
+ ];
+ }
+
$errors = [];
// Validate input
@@ -732,6 +745,37 @@
return null;
}
+ /**
+ * Validate if plan change is possible
+ */
+ protected static function validatePlan(User $user, $plan_id): bool
+ {
+ // Note: For now only mode=token plans can be changed from/into
+ // Note: For now old and new plan title must use the same prefix
+ // Note: Checking the current plan also makes sure this is allowed only on wallet owners
+
+ $plan = Plan::withObjectTenantContext($user)->find($plan_id);
+
+ if (!$plan || $plan->mode != Plan::MODE_TOKEN) {
+ return false;
+ }
+
+ $current_plan_id = $user->getSetting('plan_id');
+
+ if (!$current_plan_id) {
+ return false;
+ }
+
+ $current_plan = Plan::find($current_plan_id);
+
+ if (!$current_plan || $current_plan->mode != Plan::MODE_TOKEN) {
+ return false;
+ }
+
+ // Make sure plan title's prefix is the same
+ return explode('-', $plan->title)[0] === explode('-', $current_plan->title)[0];
+ }
+
/**
* Activate password reset code (if set), and assign it to a user.
*
diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php
--- a/src/tests/Feature/Controller/UsersTest.php
+++ b/src/tests/Feature/Controller/UsersTest.php
@@ -52,6 +52,7 @@
$user->status |= User::STATUS_IMAP_READY | User::STATUS_LDAP_READY | User::STATUS_ACTIVE;
$user->save();
Plan::withEnvTenantContext()->where('title', 'individual')->update(['mode' => 'email']);
+ Plan::withEnvTenantContext()->where('name', 'Test')->delete();
$user->setSettings(['plan_id' => null]);
}
@@ -82,6 +83,7 @@
$user->status |= User::STATUS_IMAP_READY | User::STATUS_LDAP_READY | User::STATUS_ACTIVE;
$user->save();
Plan::withEnvTenantContext()->where('title', 'individual')->update(['mode' => 'email']);
+ Plan::withEnvTenantContext()->where('name', 'Test')->delete();
$user->setSettings(['plan_id' => null]);
$folder = $this->getTestSharedFolder('folder-mail@kolab.org');
$folder->setAliases([]);
@@ -1499,6 +1501,82 @@
);
}
+ /**
+ * Test updating a user plan
+ */
+ public function testUpdatePlan(): void
+ {
+ Queue::fake();
+
+ $user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
+ $plan = Plan::withEnvTenantContext()->where('title', 'individual')->first();
+
+ $plan1 = Plan::create([
+ 'title' => 'user-test1',
+ 'name' => 'Test',
+ 'description' => 'Test',
+ 'mode' => Plan::MODE_TOKEN,
+ ]);
+ $plan2 = Plan::create([
+ 'title' => 'user-test2',
+ 'name' => 'Test',
+ 'description' => 'Test',
+ 'mode' => Plan::MODE_TOKEN,
+ ]);
+ $plan3 = Plan::create([
+ 'title' => 'device-test',
+ 'name' => 'Test',
+ 'description' => 'Test',
+ 'mode' => Plan::MODE_TOKEN,
+ ]);
+
+ // User has no plan
+ $post = ['plan_id' => $plan2->id];
+ $response = $this->actingAs($user)->put("/api/v4/users/{$user->id}", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertSame(['plan_id' => ['Invalid value']], $json['errors']);
+
+ $user->setSetting('plan_id', $plan1->id);
+
+ // Invalid plan id
+ $post = ['plan_id' => 'unknown'];
+ $response = $this->actingAs($user)->put("/api/v4/users/{$user->id}", $post);
+ $response->assertStatus(422);
+
+ // Non-token plan
+ $post = ['plan_id' => $plan->id];
+ $response = $this->actingAs($user)->put("/api/v4/users/{$user->id}", $post);
+ $response->assertStatus(422);
+
+ // From test- to device-
+ $post = ['plan_id' => $plan3->id];
+ $response = $this->actingAs($user)->put("/api/v4/users/{$user->id}", $post);
+ $response->assertStatus(422);
+
+ // Successful plan change
+ $post = ['plan_id' => $plan2->id];
+ $response = $this->actingAs($user)->put("/api/v4/users/{$user->id}", $post);
+ $response->assertStatus(200);
+
+ $this->assertSame($plan2->id, $user->getSetting('plan_id'));
+
+ // Also allow to use the current plan
+ $post = ['plan_id' => $plan2->id];
+ $response = $this->actingAs($user)->put("/api/v4/users/{$user->id}", $post);
+ $response->assertStatus(200);
+
+ // Make sure an empty plan does not unset the user plan
+ $post = ['plan_id' => null];
+ $response = $this->actingAs($user)->put("/api/v4/users/{$user->id}", $post);
+ $response->assertStatus(200);
+
+ $this->assertSame($plan2->id, $user->getSetting('plan_id'));
+ }
+
/**
* Test user data response used in show and info actions
*/

File Metadata

Mime Type
text/plain
Expires
Thu, Apr 2, 11:43 PM (18 h, 23 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18821543
Default Alt Text
D5679.1775173425.diff (6 KB)

Event Timeline