-
+
|
{{ sku.name }}
@@ -235,6 +240,65 @@
}
})
},
+ onInputSku(e) {
+ let input = e.target
+ let sku = this.findSku(input.value)
+ let required = []
+
+ // We use 'readonly', not 'disabled', because we might want to handle
+ // input events. For example to display an error when someone clicks
+ // the locked input
+ if (input.readOnly) {
+ input.checked = !input.checked
+ // TODO: Display an alert explaining why it's locked
+ return
+ }
+
+ // TODO: Following code might not work if we change definition of forbidden/required
+ // or we just need more sophisticated SKU dependency rules
+
+ if (input.checked) {
+ // Check if a required SKU is selected, alert the user if not
+ (sku.required || []).forEach(title => {
+ this.skus.forEach(item => {
+ let checkbox
+ if (item.handler == title && (checkbox = $('#s' + item.id).find('input[type=checkbox]')[0])) {
+ if (!checkbox.checked) {
+ required.push(item.name)
+ }
+ }
+ })
+ })
+
+ if (required.length) {
+ input.checked = false
+ return alert(sku.name + ' requires ' + required.join(', ') + '.')
+ }
+ } else {
+ // Uncheck all dependent SKUs, e.g. when unchecking Groupware we also uncheck Activesync
+ // TODO: Should we display an alert instead?
+ this.skus.forEach(item => {
+ if (item.required && item.required.indexOf(sku.handler) > -1) {
+ $('#s' + item.id).find('input[type=checkbox]').prop('checked', false)
+ }
+ })
+ }
+
+ // Uncheck+lock/unlock conflicting SKUs
+ (sku.forbidden || []).forEach(title => {
+ this.skus.forEach(item => {
+ let checkbox
+ if (item.handler == title && (checkbox = $('#s' + item.id).find('input[type=checkbox]')[0])) {
+ if (input.checked) {
+ checkbox.checked = false
+ checkbox.readOnly = true
+ } else {
+ checkbox.readOnly = false
+ }
+ }
+ })
+ })
+ },
selectPackage(e) {
// Make sure there always is only one package selected
$('#user-packages input').prop('checked', false)
@@ -245,19 +309,20 @@
let value = input.val()
let record = input.parents('tr').first()
let sku_id = record.find('input[type=checkbox]').val()
- let sku, i
-
- for (i = 0; i < this.skus.length; i++) {
- if (this.skus[i].id == sku_id) {
- sku = this.skus[i];
- }
- }
+ let sku = this.findSku(sku_id)
// Update the label
input.prev().text(value + ' ' + sku.range.unit)
// Update the price
record.find('.price').text(this.$root.price(sku.cost * (value - sku.units_free)) + '/month')
+ },
+ findSku(id) {
+ for (let i = 0; i < this.skus.length; i++) {
+ if (this.skus[i].id == id) {
+ return this.skus[i];
+ }
+ }
}
}
}
diff --git a/src/tests/Browser.php b/src/tests/Browser.php
--- a/src/tests/Browser.php
+++ b/src/tests/Browser.php
@@ -70,6 +70,32 @@
}
/**
+ * Assert that the given element is readonly
+ */
+ public function assertReadonly($selector)
+ {
+ $element = $this->resolver->findOrFail($selector);
+ $value = $element->getAttribute('readonly');
+
+ Assert::assertTrue($value == 'true', "Element [$selector] is not readonly");
+
+ return $this;
+ }
+
+ /**
+ * Assert that the given element is not readonly
+ */
+ public function assertNotReadonly($selector)
+ {
+ $element = $this->resolver->findOrFail($selector);
+ $value = $element->getAttribute('readonly');
+
+ Assert::assertTrue($value != 'true', "Element [$selector] is not readonly");
+
+ return $this;
+ }
+
+ /**
* Assert that the given element contains specified text,
* no matter it's displayed or not.
*/
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
@@ -39,13 +39,7 @@
UserAlias::where('user_id', $john->id)
->where('alias', 'john.test@kolab.org')->delete();
- Sku::where('title', 'test')->delete();
- $storage = Sku::where('title', 'storage')->first();
- Entitlement::where([
- ['sku_id', $storage->id],
- ['entitleable_id', $john->id],
- ['cost', 25]
- ])->delete();
+ Entitlement::where('entitleable_id', $john->id)->whereIn('cost', [25, 100])->delete();
}
/**
@@ -60,13 +54,7 @@
UserAlias::where('user_id', $john->id)
->where('alias', 'john.test@kolab.org')->delete();
- Sku::where('title', 'test')->delete();
- $storage = Sku::where('title', 'storage')->first();
- Entitlement::where([
- ['sku_id', $storage->id],
- ['entitleable_id', $john->id],
- ['cost', 25]
- ])->delete();
+ Entitlement::where('entitleable_id', $john->id)->whereIn('cost', [25, 100])->delete();
parent::tearDown();
}
@@ -127,17 +115,6 @@
*/
public function testInfo(): void
{
- Sku::create([
- 'title' => 'test',
- 'name' => 'Test SKU',
- 'description' => 'The SKU for testing',
- 'cost' => 666,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Groupware',
- 'active' => true,
- ]);
-
$this->browse(function (Browser $browser) {
$browser->on(new UserList())
->click('@table tr:nth-child(2) a')
@@ -241,48 +218,57 @@
$browser->assertSeeIn('div.row:nth-child(8) label', 'Subscriptions')
->assertVisible('@skus.row:nth-child(8)')
->with('@skus', function ($browser) {
- $browser->assertElementsCount('tbody tr', 4)
- // groupware SKU
- ->assertSeeIn('tbody tr:nth-child(1) td.name', 'Groupware Features')
- ->assertSeeIn('tbody tr:nth-child(1) td.price', '5,55 CHF/month')
+ $browser->assertElementsCount('tbody tr', 5)
+ // Mailbox SKU
+ ->assertSeeIn('tbody tr:nth-child(1) td.name', 'User Mailbox')
+ ->assertSeeIn('tbody tr:nth-child(1) td.price', '4,44 CHF/month')
->assertChecked('tbody tr:nth-child(1) td.selection input')
- ->assertEnabled('tbody tr:nth-child(1) td.selection input')
+ ->assertDisabled('tbody tr:nth-child(1) td.selection input')
->assertTip(
'tbody tr:nth-child(1) td.buttons button',
- 'Groupware functions like Calendar, Tasks, Notes, etc.'
+ 'Just a mailbox'
)
- // Mailbox SKU
- ->assertSeeIn('tbody tr:nth-child(2) td.name', 'User Mailbox')
- ->assertSeeIn('tbody tr:nth-child(2) td.price', '4,44 CHF/month')
+ // Storage SKU
+ ->assertSeeIn('tbody tr:nth-child(2) td.name', 'Storage Quota')
+ ->assertSeeIn('tr:nth-child(2) td.price', '0,00 CHF/month')
->assertChecked('tbody tr:nth-child(2) td.selection input')
->assertDisabled('tbody tr:nth-child(2) td.selection input')
->assertTip(
'tbody tr:nth-child(2) td.buttons button',
- 'Just a mailbox'
+ 'Some wiggle room'
)
- // Storage SKU
- ->assertSeeIn('tbody tr:nth-child(3) td.name', 'Storage Quota')
- ->assertSeeIn('tr:nth-child(3) td.price', '0,00 CHF/month')
+ ->with(new QuotaInput('tbody tr:nth-child(2) .range-input'), function ($browser) {
+ $browser->assertQuotaValue(2)->setQuotaValue(3);
+ })
+ ->assertSeeIn('tr:nth-child(2) td.price', '0,25 CHF/month')
+ // groupware SKU
+ ->assertSeeIn('tbody tr:nth-child(3) td.name', 'Groupware Features')
+ ->assertSeeIn('tbody tr:nth-child(3) td.price', '5,55 CHF/month')
->assertChecked('tbody tr:nth-child(3) td.selection input')
- ->assertDisabled('tbody tr:nth-child(3) td.selection input')
+ ->assertEnabled('tbody tr:nth-child(3) td.selection input')
->assertTip(
'tbody tr:nth-child(3) td.buttons button',
- 'Some wiggle room'
+ 'Groupware functions like Calendar, Tasks, Notes, etc.'
)
- ->with(new QuotaInput('tbody tr:nth-child(3) .range-input'), function ($browser) {
- $browser->assertQuotaValue(2)->setQuotaValue(3);
- })
- ->assertSeeIn('tr:nth-child(3) td.price', '0,25 CHF/month')
- // Test SKU
- ->assertSeeIn('tbody tr:nth-child(4) td.name', 'Test SKU')
- ->assertSeeIn('tbody tr:nth-child(4) td.price', '6,66 CHF/month')
+ // 2FA SKU
+ ->assertSeeIn('tbody tr:nth-child(4) td.name', '2-Factor Authentication')
+ ->assertSeeIn('tbody tr:nth-child(4) td.price', '0,00 CHF/month')
->assertNotChecked('tbody tr:nth-child(4) td.selection input')
->assertEnabled('tbody tr:nth-child(4) td.selection input')
->assertTip(
'tbody tr:nth-child(4) td.buttons button',
- 'The SKU for testing'
+ 'Two factor authentication for webmail and administration panel'
)
- ->click('tbody tr:nth-child(4) td.selection input');
+ // ActiveSync SKU
+ ->assertSeeIn('tbody tr:nth-child(5) td.name', 'Activesync')
+ ->assertSeeIn('tbody tr:nth-child(5) td.price', '1,00 CHF/month')
+ ->assertNotChecked('tbody tr:nth-child(5) td.selection input')
+ ->assertEnabled('tbody tr:nth-child(5) td.selection input')
+ ->assertTip(
+ 'tbody tr:nth-child(5) td.buttons button',
+ 'Mobile synchronization'
+ )
+ ->click('tbody tr:nth-child(5) td.selection input');
})
->click('button[type=submit]');
})
@@ -292,7 +278,34 @@
->closeToast();
});
- $this->assertUserEntitlements($john, ['groupware', 'mailbox', 'storage', 'storage', 'storage', 'test']);
+ $expected = ['activesync', 'groupware', 'mailbox', 'storage', 'storage', 'storage'];
+ $this->assertUserEntitlements($john, $expected);
+
+ // Test subscriptions interaction
+ $browser->with('@form', function (Browser $browser) {
+ $browser->with('@skus', function ($browser) {
+ // Uncheck 'groupware', expect activesync unchecked
+ $browser->click('@sku-input-groupware')
+ ->assertNotChecked('@sku-input-groupware')
+ ->assertNotChecked('@sku-input-activesync')
+ ->assertEnabled('@sku-input-activesync')
+ ->assertNotReadonly('@sku-input-activesync')
+ // Check 'activesync', expect an alert
+ ->click('@sku-input-activesync')
+ ->assertDialogOpened('Activesync requires Groupware Features.')
+ ->acceptDialog()
+ ->assertNotChecked('@sku-input-activesync')
+ // Check '2FA', expect 'activesync' unchecked and readonly
+ ->click('@sku-input-2fa')
+ ->assertChecked('@sku-input-2fa')
+ ->assertNotChecked('@sku-input-activesync')
+ ->assertReadonly('@sku-input-activesync')
+ // Uncheck '2FA'
+ ->click('@sku-input-2fa')
+ ->assertNotChecked('@sku-input-2fa')
+ ->assertNotReadonly('@sku-input-activesync');
+ });
+ });
});
}
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
@@ -18,25 +18,26 @@
$response->assertStatus(401);
$user = $this->getTestUser('john@kolab.org');
- $domain_sku = Sku::where('title', 'domain')->first();
+ $sku = Sku::where('title', 'mailbox')->first();
$response = $this->actingAs($user)->get("api/v4/skus");
$response->assertStatus(200);
$json = $response->json();
- $this->assertCount(7, $json);
+ $this->assertCount(9, $json);
- $this->assertSame($domain_sku->id, $json[0]['id']);
- $this->assertSame($domain_sku->title, $json[0]['title']);
- $this->assertSame($domain_sku->name, $json[0]['name']);
- $this->assertSame($domain_sku->description, $json[0]['description']);
- $this->assertSame($domain_sku->cost, $json[0]['cost']);
- $this->assertSame($domain_sku->units_free, $json[0]['units_free']);
- $this->assertSame($domain_sku->period, $json[0]['period']);
- $this->assertSame($domain_sku->active, $json[0]['active']);
- $this->assertSame('domain', $json[0]['type']);
- $this->assertSame('domain', $json[0]['handler']);
+ $this->assertSame(100, $json[0]['prio']);
+ $this->assertSame($sku->id, $json[0]['id']);
+ $this->assertSame($sku->title, $json[0]['title']);
+ $this->assertSame($sku->name, $json[0]['name']);
+ $this->assertSame($sku->description, $json[0]['description']);
+ $this->assertSame($sku->cost, $json[0]['cost']);
+ $this->assertSame($sku->units_free, $json[0]['units_free']);
+ $this->assertSame($sku->period, $json[0]['period']);
+ $this->assertSame($sku->active, $json[0]['active']);
+ $this->assertSame('user', $json[0]['type']);
+ $this->assertSame('mailbox', $json[0]['handler']);
}
/**
|