From 249db8528ad8d44e3778fe16e1b1da9a04746544 Mon Sep 17 00:00:00 2001 From: tzh476 Date: Sun, 19 Jul 2026 04:30:04 +0800 Subject: [PATCH 1/3] test(domainregister): cover string quota availability check Add a regression spec for #1994 that keeps availability lookup open when quota counts arrive as numeric strings below the allowed limit. Change-Id: I855782e00e34af19e9c8cc300122bd6c4c7cb649 --- .../domainregister.component.spec.ts | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/app/domainregister/domainregister.component.spec.ts diff --git a/src/app/domainregister/domainregister.component.spec.ts b/src/app/domainregister/domainregister.component.spec.ts new file mode 100644 index 000000000..7b9dd2911 --- /dev/null +++ b/src/app/domainregister/domainregister.component.spec.ts @@ -0,0 +1,91 @@ +// --------- BEGIN RUNBOX LICENSE --------- +// Copyright (C) 2016-2026 Runbox Solutions AS (runbox.com). +// +// This file is part of Runbox 7. +// +// Runbox 7 is free software: You can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at your +// option) any later version. +// +// Runbox 7 is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Runbox 7. If not, see . +// ---------- END RUNBOX LICENSE ---------- + +import { of } from 'rxjs'; + +import { DomainRegisterComponent } from './domainregister.component'; + +describe('DomainRegisterComponent quota checks', () => { + function createComponent() { + const http = { + get: jasmine.createSpy('get').and.returnValues( + of({ + result: { + product_list: [ + { + tld: 'com', + period: '1', + price: '10.00', + supports_whois_privacy: 0, + privacy_price: '0.00' + } + ] + } + }), + of({ + result: { + domain_quota_allowed: '20', + domain_quota_used: '7' + } + }) + ), + post: jasmine.createSpy('post').and.returnValue(of({ + result: { + is_available: 0, + products: [], + privacy_products: [] + } + })) + }; + const snackBar = { + open: jasmine.createSpy('open') + }; + const rmmapi = { + me: of({ is_trial: false }) + }; + + const component = new DomainRegisterComponent( + http as any, + snackBar as any, + rmmapi as any + ); + + return { component, http, snackBar }; + } + + it('allows availability checks when string quota values are below the limit', () => { + const { component, http, snackBar } = createComponent(); + + component.domain_wanted = 'example.com'; + component.domain_quota_used = '7' as any; + component.domain_quota_allowed = '20' as any; + + component.check_avail(); + + expect(http.post).toHaveBeenCalledWith( + '/rest/v1/domain_registration/enom/check_avail', + { sld: 'example', tld: 'com' } + ); + expect(snackBar.open).not.toHaveBeenCalledWith( + 'You have reached your allowed Email Domain quota. Please purchase more Email Hosting products.', + 'Dismiss', + jasmine.anything() + ); + }); +}); From dbdb42752bcfeb86b3d8b8948370e2c7cae1dc10 Mon Sep 17 00:00:00 2001 From: tzh476 Date: Sun, 19 Jul 2026 04:30:10 +0800 Subject: [PATCH 2/3] fix(domainregister): compare email domain quota numerically Normalize the domain quota values from the API before comparing them so string counts such as 7 of 20 do not trip the quota limit path for #1994. Change-Id: Ia772aa1bbf987cc60248252a1b4a0df9a7dcfeea --- .../domainregister.component.html | 4 +-- .../domainregister.component.ts | 26 ++++++++++++++++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/app/domainregister/domainregister.component.html b/src/app/domainregister/domainregister.component.html index a776e5717..9cfe32c21 100644 --- a/src/app/domainregister/domainregister.component.html +++ b/src/app/domainregister/domainregister.component.html @@ -507,11 +507,11 @@


Here you can purchase and register new domain names.

-
+

Email Domain quota limit

You have reached your Email Domain quota limit. Your account is using {{domain_quota_used}} of its {{domain_quota_allowed}} total Email Domains. Please purchase more Email Hosting products first.

-
+

Register a new domain name

diff --git a/src/app/domainregister/domainregister.component.ts b/src/app/domainregister/domainregister.component.ts index 8e9f3d1c9..a5b8a790b 100644 --- a/src/app/domainregister/domainregister.component.ts +++ b/src/app/domainregister/domainregister.component.ts @@ -61,6 +61,20 @@ export interface ElementTld { privacy_price: string; } +export function emailDomainQuotaCount(value: unknown): number { + const count = Number(value); + return Number.isFinite(count) ? count : 0; +} + +export function isEmailDomainQuotaLimitReached(used: unknown, allowed: unknown): boolean { + const usedCount = emailDomainQuotaCount(used); + const allowedCount = emailDomainQuotaCount(allowed); + + return usedCount > 0 + && allowedCount > 0 + && usedCount >= allowedCount; +} + @Component({ // eslint-disable-next-line @angular-eslint/component-selector selector: 'domain-register', @@ -157,6 +171,10 @@ export class DomainRegisterComponent implements AfterViewInit { return this.is_available ? 'primary' : 'button'; }; + public is_domain_quota_limit_reached = function () { + return isEmailDomainQuotaLimitReached(this.domain_quota_used, this.domain_quota_allowed); + }; + public calculate_total = function () { // eslint-disable-next-line no-eval this.total_price = eval(this.selected_product.price[0].price); @@ -194,7 +212,7 @@ export class DomainRegisterComponent implements AfterViewInit { public check_avail = function () { if (this.is_btn_search_domain_disabled) { return; } - if ( this.domain_quota_used && this.domain_quota_allowed && this.domain_quota_used >= this.domain_quota_allowed ) { + if ( this.is_domain_quota_limit_reached() ) { return this.show_error('You have reached your allowed Email Domain quota. Please purchase more Email Hosting products.', 'Dismiss'); } this.is_btn_search_domain_disabled = true; @@ -820,9 +838,9 @@ export class DomainRegisterComponent implements AfterViewInit { const req = this.http.get('/rest/v1/email_hosting/domains_quota'); req.pipe(timeout(180000)) .subscribe((result: any) => { - this.domain_quota_allowed = result.result.domain_quota_allowed; - this.domain_quota_used = result.result.domain_quota_used; - if ( this.domain_quota_used >= this.domain_quota_allowed ) { + this.domain_quota_allowed = emailDomainQuotaCount(result.result.domain_quota_allowed); + this.domain_quota_used = emailDomainQuotaCount(result.result.domain_quota_used); + if ( this.is_domain_quota_limit_reached() ) { this.show_error('You have reached your allowed Email Domain quota. Please purchase more Email Hosting products.', 'Dismiss'); } }, From 8a2c84c57ceec9005a97d6f1c35d9798a012efff Mon Sep 17 00:00:00 2001 From: tzh476 Date: Wed, 22 Jul 2026 01:06:56 +0800 Subject: [PATCH 3/3] test(domainregister): mock follow-up registration calls Change-Id: I65619388cc5a1442e7d0f56d4bc1f043dbf3f567 --- .../domainregister.component.spec.ts | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/app/domainregister/domainregister.component.spec.ts b/src/app/domainregister/domainregister.component.spec.ts index 7b9dd2911..cfdeca57c 100644 --- a/src/app/domainregister/domainregister.component.spec.ts +++ b/src/app/domainregister/domainregister.component.spec.ts @@ -45,13 +45,33 @@ describe('DomainRegisterComponent quota checks', () => { } }) ), - post: jasmine.createSpy('post').and.returnValue(of({ - result: { - is_available: 0, - products: [], - privacy_products: [] - } - })) + post: jasmine.createSpy('post').and.returnValues( + of({ + result: { + is_available: 0, + products: [], + privacy_products: [] + } + }), + of({ + result: { + agreement: { + generic: null, + specific: null + } + } + }), + of({ + result: { + specific_docs: [] + } + }), + of({ + result: { + generic_docs: [] + } + }) + ) }; const snackBar = { open: jasmine.createSpy('open')