Compare commits

..
Author SHA1 Message Date
james.maesandCodex d2c2f8a9b8 ci: centralize QBit source scanning
Co-Authored-By: Codex <noreply@openai.com>
2026-10-21 10:20:46 -05:00
james.maesandCodex e422092fd4 ci: adopt central orb 0.7.12 and stable gates (#28)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 08:53:07 -05:00
james.maesandCodex 70f841c256 ci: centralize rc release completion in qqq orb (#27)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 02:25:01 -05:00
james.maesandCodex 730bb68428 ci: adopt shared orb 0.7.11 (#26)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 00:56:34 -05:00
james.maes 62c393374d Merge pull request #25 from QRun-IO/feature/central-ci
ci: adopt shared QQQ orb standard
2026-10-20 19:20:28 -05:00
james.maesandCodex c380dce8b6 ci: adopt verified central QQQ orb 0.7.3
Co-Authored-By: Codex <noreply@openai.com>
2026-10-20 19:02:42 -05:00
james.maes a8e9a34b51 Merge pull request #22 from QRun-IO/feature/crm-setup-admin-only
fix(crm): Setup card only for settings holders while setup is incomplete
2026-10-16 10:19:22 -05:00
james.maesandClaude Sonnet 5.5 26b8847b46 fix(crm): show the Setup card only to CRM settings holders while setup is incomplete
The crm_setup widget was the 2nd widget on the CRM home, rendered to every
CRM user even when complete ("Nothing to do"), and carried a provider row
(Email delivery: sends through the email qbit). That is system/config content
on a business home.

- crm_setup now requires crm.manage (the permission guarding the CRM Settings
  tables) via widget rules, plus CrmSetupVisibility, a request-time custom
  permission checker (same mechanism as crm_goals) that denies once no
  business step is missing. Denied widgets are omitted (DenyBehavior.HIDDEN)
  from metadata and render.
- Removed the Email delivery row; the checklist is business setup only.
- Help text updated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 09:10:51 -05:00
james.maes 820474805d Merge pull request #21 from QRun-IO/ci/release-rc-secrets-inherit
ci(release-rc): pass secrets so the shared workflow can sign the RC tag
2026-10-16 08:03:28 -05:00
james.maesandClaude Opus 5.5 5157f81208 ci(release-rc): pass secrets to the shared workflow so it can sign the RC tag
QRun-IO/.github#2 adds a tag-and-release job that signs vX with the
QRUN_TAG_SIGNING_KEY secret after Maven Central confirms the artifact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:22:11 -05:00
james.maes c6031c26ed Merge pull request #20 from QRun-IO/fix/producer-version-from-build
fix: qbit metadata version comes from the build
2026-10-15 01:42:56 -05:00
james.maesandClaude Opus 5.5 08e880af2e fix: the qbit metadata version comes from the build, not a hardcoded constant
The producer read a literal version, so released artifacts reported a stale
or -SNAPSHOT version. qbit-crm.properties (version=${project.version}) is
Maven-filtered and the producer loads it; tests assert producer == bundle ==
artifact version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 01:22:58 -05:00
james.maes e3f20d357e Merge pull request #18 from QRun-IO/fix/help-bundle-version
fix(help): help bundle version comes from the build, not a hardcoded SNAPSHOT
2026-10-14 22:37:20 -05:00
james.maesandClaude Opus 5.5 4710ed38b3 fix(help): the help bundle carries the artifact version (Maven-filtered) instead of a hardcoded SNAPSHOT
On release branches the bundle's version and the producer's differed, so the
bundle test that asserts they match failed, and released bundles recorded
SNAPSHOT as their provenance. help/*.yaml is now filtered:
version: ${project.version}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:05:02 -05:00
james.maes 0ebbeefa08 Merge pull request #17 from QRun-IO/feature/help-bundle
feat(help): qbit-crm help bundle via QQQ help bundles, with coverage gate
2026-10-14 20:45:30 -05:00
15 changed files with 341 additions and 90 deletions

No files matched your search

+11 -57
View File
@@ -1,60 +1,14 @@
version: 2.1
setup: true
orbs:
qqq-orb: kingsrook/qqq-orb@0.6.9
commands:
configure-package-install:
steps:
- run:
name: Keep package installation unattended
command: |
# These build machines are discarded after the job.
sudo install -d /etc/needrestart/conf.d
printf '%s\n' '$nrconf{restart} = "l";' | sudo tee /etc/needrestart/conf.d/99-qqq-ci.conf >/dev/null
qqq-orb: kingsrook/qqq-orb@0.7.12
workflows:
test_only:
jobs:
- qqq-orb/mvn_test_only:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
filters:
branches:
ignore: /(develop|release\/.*)/
publish_snapshot:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: snapshot
filters:
branches:
only: [develop]
publish_release_candidate:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: release_candidate
filters:
branches:
only: [/release\/.*/]
publish_release:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: release
filters:
branches:
ignore: /.*/
tags:
only: [/^v\d+\.\d+\.\d+$/]
setup:
jobs:
- qqq-orb/generate_pipeline:
orb_version: 0.7.12
filters:
branches:
ignore: automation/qbit-rc-pending
tags:
only: /^v\d+\.\d+\.\d+$/
+23
View File
@@ -0,0 +1,23 @@
name: CodeQL
'on':
push:
branches:
- main
- develop
- release/**
- hotfix/**
pull_request: {}
schedule:
- cron: 41 16 * * 0
workflow_dispatch: {}
permissions:
contents: read
actions: read
security-events: write
packages: read
concurrency:
group: codeql-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
analyze:
uses: QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml@c288a8864fe9054f68410e1d78f5863aea4c4d22
+62 -13
View File
@@ -1,25 +1,74 @@
# Cut the next release candidate (QRun-IO/qqq docs/QRUN-QBIT-REPO-STANDARD.md, C6):
# run this workflow by hand, or merge a develop PR labelled `release:rc`. It merges develop into
# the release line and the release/* push runs CircleCI publish_release_candidate.
name: Release candidate
on:
'on':
workflow_dispatch:
inputs:
dry_run:
description: Print the plan without merging or creating branches
description: Plan a cut without changing release state
type: boolean
default: false
finalize:
description: Retry finalization of an existing cut; do not cut another RC
type: boolean
default: false
recover:
description: Recover the same published RC after a deployment timeout; never
reupload
type: boolean
default: false
circle_build:
description: Original Circle publisher build number (recovery only)
type: string
default: ''
merge_sha:
description: Existing cut merge_sha (finalize retry only)
type: string
default: ''
bump_sha:
description: Existing cut bump_sha (finalize retry only)
type: string
default: ''
branch:
description: Existing cut branch (finalize retry only)
type: string
default: ''
version:
description: Existing cut version (finalize retry only)
type: string
default: ''
pull_request:
types: [closed]
branches: [develop]
types:
- closed
branches:
- develop
repository_dispatch:
types:
- qbit-rc-published
permissions:
contents: write
actions: read
checks: read
jobs:
release-rc:
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release:rc'))
uses: QRun-IO/.github/.github/workflows/qbit-release-rc.yml@main
if: github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch'
|| (github.event_name == 'pull_request' && github.event.pull_request.merged
== true && github.event.pull_request.base.ref == 'develop' && contains(github.event.pull_request.labels.*.name,
'release:rc'))
uses: QRun-IO/qqq-orb/.github/workflows/qbit-release-rc.yml@cc30f599e63c8e41629c99d23395bac1adb8bc44
with:
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false
}}
finalize: ${{ github.event_name == 'repository_dispatch' || (github.event_name
== 'workflow_dispatch' && inputs.finalize) }}
recover: ${{ github.event_name == 'workflow_dispatch' && inputs.recover || false
}}
circle_build: ${{ github.event_name == 'workflow_dispatch' && inputs.circle_build
|| '' }}
merge_sha: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.merge_sha
|| inputs.merge_sha || '' }}
bump_sha: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.bump_sha
|| inputs.bump_sha || '' }}
branch: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch
|| inputs.branch || '' }}
version: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.version
|| inputs.version || '' }}
secrets: inherit
+16
View File
@@ -0,0 +1,16 @@
profile: qbit
orb_version: 0.7.12
default_branch: develop
java_version: '21'
browsers: true
api_checks: false
docs_only: false
release_on_main: false
release_tag_policy: stable
test_branch_policy: non-release
publish:
snapshot: true
release_candidate: true
release: true
asciidoc: false
release_completion: true
+2
View File
@@ -2,6 +2,8 @@
## Unreleased / 0.2.0
- Read the CRM QBit metadata version from the Maven-filtered artifact version so release metadata matches the build.
- Adopt QQQ 4.1.0-RC.7 native help bundles, replacing the RC.6 stand-in loader with host-neutral CRM help, live references and the core coverage gate.
- Upgrade to Java 21, QQQ 4.1.0-RC.6 and qbit-build-parent 2.1.2.
+28
View File
@@ -135,7 +135,35 @@
</dependencies>
<build>
<resources>
<!-- help bundles carry the artifact version (QQQ help bundle provenance) -->
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>help/*.yaml</include>
<include>qbit-crm.properties</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>help/*.yaml</exclude>
<exclude>qbit-crm.properties</exclude>
</excludes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables combine.children="append">
<qbit.artifactVersion>${project.version}</qbit.artifactVersion>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
@@ -26,9 +26,12 @@
package com.kingsrook.qbits.crm;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import com.kingsrook.qbits.crm.activities.customizers.ActivityLastDateCustomizer;
import com.kingsrook.qbits.crm.activities.model.Activity;
import com.kingsrook.qbits.crm.core.customizers.CurrencyRateDateCustomizer;
@@ -86,12 +89,40 @@ public class CrmQBitProducer implements QBitMetaDataProducer<CrmQBitConfig>
{
public static final String GROUP_ID = "com.kingsrook.qbits";
public static final String ARTIFACT_ID = "crm";
public static final String VERSION = "0.2.0-SNAPSHOT";
public static final String VERSION = loadVersion();
private CrmQBitConfig qBitConfig;
/*******************************************************************************
** Read the artifact version from the Maven-filtered properties resource.
*******************************************************************************/
private static String loadVersion()
{
try(InputStream inputStream = CrmQBitProducer.class.getClassLoader().getResourceAsStream("qbit-crm.properties"))
{
if(inputStream == null)
{
throw new IllegalStateException("Missing qbit-crm.properties; build with Maven to provide the artifact version");
}
Properties properties = new Properties();
properties.load(inputStream);
String version = properties.getProperty("version");
if(version == null || version.isBlank() || version.contains("${"))
{
throw new IllegalStateException("Missing or unfiltered version in qbit-crm.properties; build with Maven to provide the artifact version");
}
return (version);
}
catch(IOException e)
{
throw new IllegalStateException("Could not read artifact version from qbit-crm.properties", e);
}
}
/*******************************************************************************
** Produce the QBitMetaData for the CRM QBit.
*******************************************************************************/
@@ -76,6 +76,11 @@ public class CrmHomeWidgets extends MetaDataProducer<MetaDataProducerMultiOutput
{
widget.getPermissionRules().withCustomPermissionChecker(new QCodeReference(CrmGoalsVisibility.class));
}
if("crm_setup".equals(name))
{
widget.withPermissionRules(CrmPermissions.hasAccessRules(CrmPermissions.MANAGE_BASE))
.getPermissionRules().withCustomPermissionChecker(new QCodeReference(CrmSetupVisibility.class));
}
return (widget);
}
}
@@ -23,8 +23,6 @@ package com.kingsrook.qbits.crm.widgets;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qbits.crm.api.CrmApi;
import com.kingsrook.qbits.crm.api.CrmReadiness;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@@ -89,6 +87,16 @@ public final class CrmSetupStatus
/*******************************************************************************
** True when any business setup step still needs attention.
*******************************************************************************/
public static boolean isIncomplete() throws QException
{
return (load().stream().anyMatch(item -> item.state() == State.TODO));
}
/*******************************************************************************
** Build the items for the current user.
*******************************************************************************/
@@ -160,14 +168,6 @@ public final class CrmSetupStatus
? new Item("Base currency", "crm_currency", State.TODO, "None: amounts have no currency prefix and conversions do nothing.", "Add your currency with Base currency ticked and rate 1.")
: new Item("Base currency", "crm_currency", State.OK, base.getFirst().getValueString("currencyCode") + " is the base currency.", null));
///////////////////////////////////////////////////////
// email: sequences and list mail need a delivery path //
///////////////////////////////////////////////////////
CrmReadiness readiness = CrmApi.readiness();
items.add(readiness == CrmReadiness.NOT_CONFIGURED
? new Item("Email delivery", "crm_email_template", State.OPTIONAL, "No email path: sequences and list email cannot send.",
"Install and configure the email qbit, or set a messaging provider on the CRM configuration.")
: new Item("Email delivery", "crm_email_template", State.OK, readiness == CrmReadiness.QBIT_EMAIL ? "Sends through the email qbit." : "Sends through core messaging.", null));
return (items);
}
@@ -0,0 +1,64 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2026. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.crm.widgets;
import com.kingsrook.qbits.crm.security.CrmPermissions;
import com.kingsrook.qqq.backend.core.actions.permissions.CustomPermissionChecker;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
/*******************************************************************************
** Show the Setup card only to CRM settings holders (crm.manage) and only while
** a business setup step is still missing. Evaluated at request time, so a
** completed setup drops the card from per-session metadata and from render.
*******************************************************************************/
public class CrmSetupVisibility implements CustomPermissionChecker
{
/*******************************************************************************
** Require crm.hasAccess and crm.manage.hasAccess, then an incomplete setup.
*******************************************************************************/
@Override
public void checkPermissionsThrowing(AbstractActionInput input, MetaDataWithPermissionRules metadata) throws QPermissionDeniedException
{
if(QContext.getQSession() == null || !QContext.getQSession().hasPermission(CrmPermissions.APP_ACCESS) || !QContext.getQSession().hasPermission(CrmPermissions.MANAGE))
{
throw (new QPermissionDeniedException("CRM settings access is required."));
}
try
{
if(CrmSetupStatus.isIncomplete())
{
return;
}
}
catch(QException e)
{
throw (new QPermissionDeniedException("Setup status is not available.", e));
}
throw (new QPermissionDeniedException("CRM setup is complete."));
}
}
+2 -2
View File
@@ -1,11 +1,11 @@
bundle: qbit-crm
version: "0.2.0-SNAPSHOT"
version: ${project.version}
layer: QBIT
entries:
- key: app:crm;slot:help
summary: "Manage relationships, sales opportunities and consent."
content: |
Use **Contacts**, **Companies**, **Deals**, **Activities**, **Web Leads** and **Lists** for daily work. The Setup card on this page shows what still needs configuring: the default pipeline and its stages, lifecycle stages, the base currency and email delivery. Ask your administrator which reference data and base currency are configured for your installation. Configure pipelines, outcomes and outreach in Settings. Marketing email requires recorded consent and a ready email transport.
Use **Contacts**, **Companies**, **Deals**, **Activities**, **Web Leads** and **Lists** for daily work. The Setup card on this page shows what still needs configuring: the default pipeline and its stages, lifecycle stages, and the base currency. It only appears for CRM settings users, and only while a step is missing. Ask your administrator which reference data and base currency are configured for your installation. Configure pipelines, outcomes and outreach in Settings. Marketing email requires recorded consent and a ready email transport.
- key: table:crm_job_state;slot:help
summary: "Internal maintenance checkpoints."
content: |
+1
View File
@@ -0,0 +1 @@
version=${project.version}
@@ -110,7 +110,7 @@ class CrmPublicationHelpTest extends BaseTest
assertThat(HelpBundles.of(instance).getBundles()).singleElement().satisfies(bundle ->
{
assertThat(bundle.getId()).isEqualTo("qbit-crm");
assertThat(bundle.getVersion()).isEqualTo(CrmQBitProducer.VERSION);
assertThat(bundle.getVersion()).isEqualTo(System.getProperty("qbit.artifactVersion")).isEqualTo(CrmQBitProducer.VERSION);
assertThat(bundle.getLayer().name()).isEqualTo("QBIT");
});
HelpBundles.syncOnStartup(instance);
@@ -62,7 +62,7 @@ class CrmQBitProducerTest extends BaseTest
assertThat(metaData).isNotNull();
assertThat(metaData.getGroupId()).isEqualTo(CrmQBitProducer.GROUP_ID);
assertThat(metaData.getArtifactId()).isEqualTo(CrmQBitProducer.ARTIFACT_ID);
assertThat(metaData.getVersion()).isEqualTo(CrmQBitProducer.VERSION);
assertThat(metaData.getVersion()).isEqualTo(System.getProperty("qbit.artifactVersion")).isEqualTo(CrmQBitProducer.VERSION);
}
@@ -114,7 +114,7 @@ class CrmQBitProducerTest extends BaseTest
{
assertThat(CrmQBitProducer.GROUP_ID).isEqualTo("com.kingsrook.qbits");
assertThat(CrmQBitProducer.ARTIFACT_ID).isEqualTo("crm");
assertThat(CrmQBitProducer.VERSION).isEqualTo("0.2.0-SNAPSHOT");
assertThat(CrmQBitProducer.VERSION).isEqualTo(System.getProperty("qbit.artifactVersion"));
}
/*******************************************************************************
@@ -23,12 +23,21 @@ package com.kingsrook.qbits.crm.widgets;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qbits.crm.BaseTest;
import com.kingsrook.qbits.crm.security.CrmPermissions;
import com.kingsrook.qqq.backend.core.actions.dashboard.RenderWidgetAction;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionEnforcement;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.TableData;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/*******************************************************************************
@@ -38,19 +47,18 @@ import static org.assertj.core.api.Assertions.assertThat;
class CrmSetupStatusTest extends BaseTest
{
/*******************************************************************************
** Nothing configured: the four things the CRM needs are flagged.
** Nothing configured: the business setup the CRM needs are flagged.
*******************************************************************************/
@Test
void emptyInstallNeedsSetup() throws Exception
{
Map<String, CrmSetupStatus.Item> items = items();
assertThat(items).containsOnlyKeys("Default pipeline", "Pipeline stages", "Lifecycle stages", "Base currency", "Email delivery");
assertThat(items).containsOnlyKeys("Default pipeline", "Pipeline stages", "Lifecycle stages", "Base currency");
assertThat(items.get("Default pipeline").state()).isEqualTo(CrmSetupStatus.State.TODO);
assertThat(items.get("Default pipeline").detail()).contains("Convert lead fails");
assertThat(items.get("Pipeline stages").state()).isEqualTo(CrmSetupStatus.State.TODO);
assertThat(items.get("Lifecycle stages").detail()).contains("Lead and Opportunity");
assertThat(items.get("Base currency").state()).isEqualTo(CrmSetupStatus.State.TODO);
assertThat(items.get("Email delivery").state()).isNotEqualTo(CrmSetupStatus.State.TODO);
}
@@ -106,7 +114,7 @@ class CrmSetupStatusTest extends BaseTest
{
assertThat(CrmHomeWidgets.HOME).contains("crm_setup");
TableData data = (TableData) CrmHomeWidgetsTest.render("crm_setup");
assertThat(data.getRows()).hasSize(5);
assertThat(data.getRows()).hasSize(4);
CrmHomeWidgetsTest.assertCell(data, 0, "first", "Default pipeline");
assertThat(data.getRows().get(0).get("second").toString()).contains("Needs setup");
@@ -114,6 +122,76 @@ class CrmSetupStatusTest extends BaseTest
/*******************************************************************************
** The checklist is business setup only: no email-provider row.
*******************************************************************************/
@Test
void noEmailProviderRow() throws Exception
{
assertThat(items().keySet()).noneMatch(label -> label.toLowerCase().contains("email"));
TableData data = (TableData) CrmHomeWidgetsTest.render("crm_setup");
assertThat(data.getRows().toString()).doesNotContain("email qbit").doesNotContain("Email delivery");
}
/*******************************************************************************
** Settings holders get the card while setup is incomplete, and not once complete.
*******************************************************************************/
@Test
void settingsHolderSeesCardOnlyWhileIncomplete() throws Exception
{
assertThat(PermissionsHelper.hasWidgetPermission(new RenderWidgetInput(), "crm_setup")).isTrue();
completeSetup();
assertThat(CrmSetupStatus.isIncomplete()).isFalse();
assertThat(PermissionsHelper.hasWidgetPermission(new RenderWidgetInput(), "crm_setup")).isFalse();
assertThatThrownBy(() -> renderEnforced()).isInstanceOf(QPermissionDeniedException.class);
}
/*******************************************************************************
** A CRM user without the settings permission never gets the card, incomplete or not.
*******************************************************************************/
@Test
void nonSettingsUserNeverSeesCard() throws Exception
{
QContext.getQSession().setPermissions(Set.of(CrmPermissions.APP_ACCESS, CrmPermissions.READ, CrmPermissions.WRITE));
assertThat(CrmSetupStatus.isIncomplete()).isTrue();
assertThat(PermissionsHelper.hasWidgetPermission(new RenderWidgetInput(), "crm_setup")).isFalse();
assertThatThrownBy(() -> renderEnforced()).isInstanceOf(QPermissionDeniedException.class);
}
/*******************************************************************************
** Render the setup widget the way the UI API does, with permissions enforced.
*******************************************************************************/
private void renderEnforced() throws Exception
{
RenderWidgetInput input = new RenderWidgetInput().withWidgetMetaData(QContext.getQInstance().getWidget("crm_setup"));
input.setPermissionEnforcement(PermissionEnforcement.ENFORCE);
new RenderWidgetAction().execute(input);
}
/*******************************************************************************
** Seed everything the checklist asks for.
*******************************************************************************/
private void completeSetup() throws Exception
{
CrmHomeWidgetsTest.seed("crm_pipeline", new QRecord().withValue("id", 1).withValue("name", "Sales").withValue("isDefault", true));
CrmHomeWidgetsTest.seed("crm_pipeline_stage", new QRecord().withValue("id", 2).withValue("pipelineId", 1).withValue("name", "Won").withValue("sortOrder", 2).withValue("isClosedWon", true));
CrmHomeWidgetsTest.seed("crm_pipeline_stage", new QRecord().withValue("id", 3).withValue("pipelineId", 1).withValue("name", "Lost").withValue("sortOrder", 3).withValue("isClosedLost", true));
CrmHomeWidgetsTest.seed("crm_lifecycle_stage", new QRecord().withValue("id", 1).withValue("name", "Lead"));
CrmHomeWidgetsTest.seed("crm_lifecycle_stage", new QRecord().withValue("id", 2).withValue("name", "Opportunity"));
CrmHomeWidgetsTest.seed("crm_currency", new QRecord().withValue("currencyCode", "USD").withValue("isBaseCurrency", true));
}
/*******************************************************************************
**
*******************************************************************************/