feat(bird): add control plane configuration
This commit is contained in:
@@ -35,6 +35,12 @@ export const Bird = {
|
||||
},
|
||||
config: Config,
|
||||
functions: {
|
||||
getHealth: async () => {
|
||||
return authenticatedRequest(
|
||||
"/bird/health",
|
||||
"GET"
|
||||
);
|
||||
},
|
||||
createVoiceCall: async (workspaceId, channelId, payload) => {
|
||||
return authenticatedRequest(
|
||||
"/bird/voice/calls" + withWorkspaceAndChannel(workspaceId, channelId),
|
||||
|
||||
@@ -44,6 +44,15 @@ export const Config = {
|
||||
return Config.set("server_url", value);
|
||||
},
|
||||
},
|
||||
workspaceId: {
|
||||
get: async () => {
|
||||
return Config.get("workspaceId");
|
||||
},
|
||||
set: async (value) => {
|
||||
return Config.set("workspaceId", value);
|
||||
},
|
||||
},
|
||||
// Kept for installations that have not migrated the historical typo yet.
|
||||
workplaceId: {
|
||||
get: async () => {
|
||||
return Config.get("workplaceId");
|
||||
@@ -60,6 +69,54 @@ export const Config = {
|
||||
return Config.set("channelId", value);
|
||||
},
|
||||
},
|
||||
participantId: {
|
||||
get: async () => Config.get("participantId"),
|
||||
set: async (value) => Config.set("participantId", value),
|
||||
},
|
||||
allowed_channel_ids_json: {
|
||||
get: async () => Config.get("allowed_channel_ids_json"),
|
||||
set: async (value) => Config.set("allowed_channel_ids_json", value),
|
||||
},
|
||||
control_plane_token: {
|
||||
get: async () => Config.get("control_plane_token"),
|
||||
set: async (value) => Config.set("control_plane_token", value),
|
||||
},
|
||||
webhook_signing_key: {
|
||||
get: async () => Config.get("webhook_signing_key"),
|
||||
set: async (value) => Config.set("webhook_signing_key", value),
|
||||
},
|
||||
webhook_public_url: {
|
||||
get: async () => Config.get("webhook_public_url"),
|
||||
set: async (value) => Config.set("webhook_public_url", value),
|
||||
},
|
||||
webhook_replay_window_seconds: {
|
||||
get: async () => Config.get("webhook_replay_window_seconds"),
|
||||
set: async (value) => Config.set("webhook_replay_window_seconds", value),
|
||||
},
|
||||
flow_shared_secret: {
|
||||
get: async () => Config.get("flow_shared_secret"),
|
||||
set: async (value) => Config.set("flow_shared_secret", value),
|
||||
},
|
||||
flow_policy_json: {
|
||||
get: async () => Config.get("flow_policy_json"),
|
||||
set: async (value) => Config.set("flow_policy_json", value),
|
||||
},
|
||||
template_policy_json: {
|
||||
get: async () => Config.get("template_policy_json"),
|
||||
set: async (value) => Config.set("template_policy_json", value),
|
||||
},
|
||||
control_plane_enabled: {
|
||||
set: async (value) => Config.set("control_plane_enabled", value),
|
||||
},
|
||||
flow_enabled: {
|
||||
set: async (value) => Config.set("flow_enabled", value),
|
||||
},
|
||||
operations_actions_enabled: {
|
||||
set: async (value) => Config.set("operations_actions_enabled", value),
|
||||
},
|
||||
outbound_messages_enabled: {
|
||||
set: async (value) => Config.set("outbound_messages_enabled", value),
|
||||
},
|
||||
},
|
||||
enabled: {
|
||||
get: async () => {
|
||||
|
||||
@@ -1189,6 +1189,70 @@
|
||||
"zip": "@:{'templates.generated.compat.common.zip'}"
|
||||
},
|
||||
"configuration": {
|
||||
"bird": {
|
||||
"api_key": "API key",
|
||||
"api_key_desc": "The Bird access key. The saved value is never displayed.",
|
||||
"api_settings": "API connection",
|
||||
"api_settings_desc": "Credentials and identifiers used by the Pleno backend to connect to Bird.",
|
||||
"allowed_channels": "Allowed channel IDs (JSON)",
|
||||
"allowed_channels_desc": "An explicit JSON array of Bird channel IDs owned by Pleno. The legacy default channel is used only while this list is empty.",
|
||||
"automation": "Flows and controlled writes",
|
||||
"automation_desc": "All switches are disabled by default. Enable them only after the protected activation review.",
|
||||
"channel_id": "Default channel ID",
|
||||
"channel_id_desc": "The default Bird channel used by existing voice operations.",
|
||||
"channels": "Available channels",
|
||||
"config_unavailable": "Bird configuration could not be loaded. No settings were changed.",
|
||||
"control_plane": "Control Plane gateway",
|
||||
"control_plane_desc": "Authenticate the local Control Plane through the Pleno backend. Never place the Bird access key in Control Plane.",
|
||||
"control_plane_enable": "Enable the Control Plane gateway",
|
||||
"control_plane_enable_desc": "Permit the narrow service-bearer API after credentials and channel ownership are verified.",
|
||||
"control_plane_token": "Control Plane service token",
|
||||
"control_plane_token_desc": "The narrow Pleno service bearer. The saved value is never displayed.",
|
||||
"enable": "Enable Bird",
|
||||
"enable_desc": "Allow the Pleno backend to use the configured Bird integration.",
|
||||
"general_settings": "General settings",
|
||||
"general_settings_desc": "Enable or disable Bird without changing saved credentials.",
|
||||
"flow_enable": "Enable deterministic Bird Flow decisions",
|
||||
"flow_enable_desc": "Allow only policy-matched tag, assign, snooze, and close decisions.",
|
||||
"flow_policy": "Flow policy (JSON)",
|
||||
"flow_policy_desc": "Versioned deterministic rules evaluated by the Pleno backend.",
|
||||
"flow_secret": "Flow shared secret",
|
||||
"flow_secret_desc": "Authenticates timestamp-bound Bird Flow requests. The saved value is never displayed.",
|
||||
"health_title": "Connection health",
|
||||
"not_available": "Not available",
|
||||
"not_configured": "Not configured",
|
||||
"operations_enable": "Enable confirmed voice operations",
|
||||
"operations_enable_desc": "Permit only typed, explicitly confirmed, idempotent operations exposed by the gateway.",
|
||||
"outbound_enable": "Enable confirmed conversation replies",
|
||||
"outbound_enable_desc": "Permit approved replies and separately allowlisted immutable templates through the gateway.",
|
||||
"participant_id": "Bird participant ID",
|
||||
"participant_id_desc": "The Bird participant identity used as the sender for confirmed replies and immutable templates.",
|
||||
"provider_check": "Provider check",
|
||||
"read_only_health": "This read-only check does not place calls, send messages, or change Bird data.",
|
||||
"refresh_health": "Refresh health",
|
||||
"server_url": "Server URL",
|
||||
"server_url_desc": "The Bird API base URL.",
|
||||
"state": {
|
||||
"degraded": "Degraded",
|
||||
"disabled": "Disabled",
|
||||
"healthy": "Healthy",
|
||||
"needs_configuration": "Needs configuration",
|
||||
"unavailable": "Unavailable"
|
||||
},
|
||||
"subtitle": "Configure Bird credentials and inspect read-only connection and channel health.",
|
||||
"title": "Bird configuration",
|
||||
"template_policy": "Immutable template allowlist (JSON)",
|
||||
"template_policy_desc": "Exact template project IDs, versions, locales, and structured parameter rules.",
|
||||
"unnamed_channel": "Unnamed channel",
|
||||
"replay_window": "Webhook replay window (seconds)",
|
||||
"replay_window_desc": "Maximum accepted Bird webhook timestamp skew.",
|
||||
"webhook_key": "Webhook signing key",
|
||||
"webhook_key_desc": "Verifies Bird notification signatures. The saved value is never displayed.",
|
||||
"webhook_url": "Exact public webhook URL",
|
||||
"webhook_url_desc": "The HTTPS URL included exactly in Bird signature verification.",
|
||||
"workspace_id": "Workspace ID",
|
||||
"workspace_id_desc": "The canonical Bird workspace identifier. Existing workplaceId values remain readable during migration."
|
||||
},
|
||||
"backups": {
|
||||
"active_jobs": "@:{'templates.generated.compat.configuration.backups.active_jobs'}",
|
||||
"actions": "@:{'templates.generated.compat.configuration.backups.actions'}",
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"configuration": {
|
||||
"bird": {
|
||||
"api_key": "API key",
|
||||
"api_key_desc": "The Bird access key. The saved value is never displayed.",
|
||||
"api_settings": "API connection",
|
||||
"api_settings_desc": "Credentials and identifiers used by the Pleno backend to connect to Bird.",
|
||||
"allowed_channels": "Allowed channel IDs (JSON)",
|
||||
"allowed_channels_desc": "An explicit JSON array of Bird channel IDs owned by Pleno. The legacy default channel is used only while this list is empty.",
|
||||
"automation": "Flows and controlled writes",
|
||||
"automation_desc": "All switches are disabled by default. Enable them only after the protected activation review.",
|
||||
"channel_id": "Default channel ID",
|
||||
"channel_id_desc": "The default Bird channel used by existing voice operations.",
|
||||
"channels": "Available channels",
|
||||
"config_unavailable": "Bird configuration could not be loaded. No settings were changed.",
|
||||
"control_plane": "Control Plane gateway",
|
||||
"control_plane_desc": "Authenticate the local Control Plane through the Pleno backend. Never place the Bird access key in Control Plane.",
|
||||
"control_plane_enable": "Enable the Control Plane gateway",
|
||||
"control_plane_enable_desc": "Permit the narrow service-bearer API after credentials and channel ownership are verified.",
|
||||
"control_plane_token": "Control Plane service token",
|
||||
"control_plane_token_desc": "The narrow Pleno service bearer. The saved value is never displayed.",
|
||||
"enable": "Enable Bird",
|
||||
"enable_desc": "Allow the Pleno backend to use the configured Bird integration.",
|
||||
"general_settings": "General settings",
|
||||
"general_settings_desc": "Enable or disable Bird without changing saved credentials.",
|
||||
"flow_enable": "Enable deterministic Bird Flow decisions",
|
||||
"flow_enable_desc": "Allow only policy-matched tag, assign, snooze, and close decisions.",
|
||||
"flow_policy": "Flow policy (JSON)",
|
||||
"flow_policy_desc": "Versioned deterministic rules evaluated by the Pleno backend.",
|
||||
"flow_secret": "Flow shared secret",
|
||||
"flow_secret_desc": "Authenticates timestamp-bound Bird Flow requests. The saved value is never displayed.",
|
||||
"health_title": "Connection health",
|
||||
"not_available": "Not available",
|
||||
"not_configured": "Not configured",
|
||||
"operations_enable": "Enable confirmed voice operations",
|
||||
"operations_enable_desc": "Permit only typed, explicitly confirmed, idempotent operations exposed by the gateway.",
|
||||
"outbound_enable": "Enable confirmed conversation replies",
|
||||
"outbound_enable_desc": "Permit approved replies and separately allowlisted immutable templates through the gateway.",
|
||||
"participant_id": "Bird participant ID",
|
||||
"participant_id_desc": "The Bird participant identity used as the sender for confirmed replies and immutable templates.",
|
||||
"provider_check": "Provider check",
|
||||
"read_only_health": "This read-only check does not place calls, send messages, or change Bird data.",
|
||||
"refresh_health": "Refresh health",
|
||||
"server_url": "Server URL",
|
||||
"server_url_desc": "The Bird API base URL.",
|
||||
"state": {
|
||||
"degraded": "Degraded",
|
||||
"disabled": "Disabled",
|
||||
"healthy": "Healthy",
|
||||
"needs_configuration": "Needs configuration",
|
||||
"unavailable": "Unavailable"
|
||||
},
|
||||
"subtitle": "Configure Bird credentials and inspect read-only connection and channel health.",
|
||||
"title": "Bird configuration",
|
||||
"template_policy": "Immutable template allowlist (JSON)",
|
||||
"template_policy_desc": "Exact template project IDs, versions, locales, and structured parameter rules.",
|
||||
"unnamed_channel": "Unnamed channel",
|
||||
"replay_window": "Webhook replay window (seconds)",
|
||||
"replay_window_desc": "Maximum accepted Bird webhook timestamp skew.",
|
||||
"webhook_key": "Webhook signing key",
|
||||
"webhook_key_desc": "Verifies Bird notification signatures. The saved value is never displayed.",
|
||||
"webhook_url": "Exact public webhook URL",
|
||||
"webhook_url_desc": "The HTTPS URL included exactly in Bird signature verification.",
|
||||
"workspace_id": "Workspace ID",
|
||||
"workspace_id_desc": "The canonical Bird workspace identifier. Existing workplaceId values remain readable during migration."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +1,470 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
|
||||
import ConfigurationCategory from "@/components/displays/superuser/configuration/ConfigurationCategory.vue";
|
||||
import ConfigurationInput from "@/components/displays/superuser/configuration/ConfigurationInput.vue";
|
||||
import ConfigurationSecretKey from "@/components/displays/superuser/configuration/ConfigurationSecretKey.vue";
|
||||
import ConfigurationSwitch from "@/components/displays/superuser/configuration/ConfigurationSwitch.vue";
|
||||
import PageTitle from "@/components/global/PageTitle.vue";
|
||||
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
|
||||
import { ref } from 'vue';
|
||||
import ConfigurationSubPageWrapper
|
||||
from "@/views/dashboards/superUserDashboard/configuration/ConfigurationSubPageWrapper.vue";
|
||||
import ConfigurationCategory from "@/components/displays/superuser/configuration/ConfigurationCategory.vue";
|
||||
import ConfigurationSwitch from "@/components/displays/superuser/configuration/ConfigurationSwitch.vue";
|
||||
import ConfigurationSecretKey from "@/components/displays/superuser/configuration/ConfigurationSecretKey.vue";
|
||||
import ConfigurationInput from "@/components/displays/superuser/configuration/ConfigurationInput.vue";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
// Get the department from the route
|
||||
const moduleConfig = ref([]);
|
||||
const BIRD_SECRET_VARIABLES = new Set([
|
||||
"api_key",
|
||||
"control_plane_token",
|
||||
"webhook_signing_key",
|
||||
"flow_shared_secret",
|
||||
]);
|
||||
const configLoading = ref(true);
|
||||
const configUnavailable = ref(false);
|
||||
const healthLoading = ref(false);
|
||||
const healthError = ref(false);
|
||||
const health = ref({
|
||||
enabled: false,
|
||||
configured: false,
|
||||
healthy: false,
|
||||
workspaceId: "",
|
||||
channelId: "",
|
||||
check: "",
|
||||
channels: [],
|
||||
});
|
||||
|
||||
const getModuleConfigEntry = (variable) =>
|
||||
moduleConfig.value.find((config) => config.variable === variable);
|
||||
|
||||
const enabled = ref(false);
|
||||
const module_config = ref([]);
|
||||
const getModuleConfigValue = (variable) =>
|
||||
getModuleConfigEntry(variable)?.value ?? "";
|
||||
|
||||
const getEnabled = async () => {
|
||||
await SessionUser.superUser.modules.bird.config.enabled.get().then((response) => {
|
||||
enabled.value = response.data.data;
|
||||
}).catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
const isModuleSecretSet = (variable) => {
|
||||
const config = getModuleConfigEntry(variable);
|
||||
return config?.isSecret === true
|
||||
? config.isSet === true
|
||||
: config?.value !== null && config?.value !== undefined && config.value !== "";
|
||||
};
|
||||
|
||||
const configuredWorkspaceId = computed(
|
||||
() => getModuleConfigValue("workspaceId") || getModuleConfigValue("workplaceId")
|
||||
);
|
||||
|
||||
const normalizedChannels = computed(() =>
|
||||
Array.isArray(health.value.channels)
|
||||
? health.value.channels.filter((channel) => channel && typeof channel === "object")
|
||||
: []
|
||||
);
|
||||
|
||||
const healthState = computed(() => {
|
||||
if (healthError.value) {
|
||||
return "unavailable";
|
||||
}
|
||||
if (!health.value.enabled) {
|
||||
return "disabled";
|
||||
}
|
||||
if (!health.value.configured) {
|
||||
return "needs_configuration";
|
||||
}
|
||||
return health.value.healthy ? "healthy" : "degraded";
|
||||
});
|
||||
|
||||
const healthStateClass = computed(() => ({
|
||||
"is-success": healthState.value === "healthy",
|
||||
"is-warning": ["disabled", "needs_configuration"].includes(healthState.value),
|
||||
"is-danger": ["degraded", "unavailable"].includes(healthState.value),
|
||||
}));
|
||||
|
||||
const normalizeHealth = (response) => {
|
||||
const data = response?.data?.data ?? response?.data ?? {};
|
||||
return {
|
||||
enabled: data.enabled === true,
|
||||
configured: data.configured === true,
|
||||
healthy: data.providerReachable === true || data.healthy === true,
|
||||
workspaceId: typeof data.workspaceId === "string" ? data.workspaceId : "",
|
||||
channelId: typeof data.channelId === "string" ? data.channelId : "",
|
||||
check: typeof data.check === "string" ? data.check : "",
|
||||
channels: Array.isArray(data.channels) ? data.channels : [],
|
||||
};
|
||||
};
|
||||
|
||||
const getModuleConfig = async () => {
|
||||
await SessionUser.superUser.modules.bird.config.get_all().then((response) => {
|
||||
let tmp_module_config = response.data.data;
|
||||
let tmp_module_config_array = [];
|
||||
for (const value of Object.values(tmp_module_config)) {
|
||||
console.log(`${value.variable}: ${value.value}`);
|
||||
tmp_module_config_array.push({
|
||||
variable: value.variable,
|
||||
value: value.value,
|
||||
configLoading.value = true;
|
||||
configUnavailable.value = false;
|
||||
|
||||
try {
|
||||
const response = await SessionUser.superUser.modules.bird.config.get_all();
|
||||
const config = response?.data?.data ?? {};
|
||||
moduleConfig.value = Object.values(config)
|
||||
.filter((value) => value && typeof value.variable === "string")
|
||||
.map((value) => {
|
||||
// The explicit names protect staggered deployments where an older API
|
||||
// does not yet provide isSecret metadata.
|
||||
const isSecret = value.isSecret === true
|
||||
|| BIRD_SECRET_VARIABLES.has(value.variable);
|
||||
return {
|
||||
variable: value.variable,
|
||||
value: isSecret ? "" : value.value,
|
||||
isSecret,
|
||||
isSet: value.isSet === true || (isSecret && value.value !== ""),
|
||||
};
|
||||
});
|
||||
}
|
||||
module_config.value = tmp_module_config_array;
|
||||
console.log(module_config.value);
|
||||
}).catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
} catch {
|
||||
moduleConfig.value = [];
|
||||
configUnavailable.value = true;
|
||||
} finally {
|
||||
configLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const getModuleConfigValue = (variable) => {
|
||||
const config = module_config.value.find((config) => config.variable === variable);
|
||||
if (config) {
|
||||
console.log(`Found value for ${variable}: ${config.value}`);
|
||||
} else {
|
||||
console.log(`No value found for ${variable}`);
|
||||
const refreshHealth = async () => {
|
||||
healthLoading.value = true;
|
||||
healthError.value = false;
|
||||
|
||||
try {
|
||||
const response = await SessionUser.superUser.modules.bird.functions.getHealth();
|
||||
health.value = normalizeHealth(response);
|
||||
} catch {
|
||||
healthError.value = true;
|
||||
health.value = {
|
||||
enabled: false,
|
||||
configured: false,
|
||||
healthy: false,
|
||||
workspaceId: "",
|
||||
channelId: "",
|
||||
check: "",
|
||||
channels: [],
|
||||
};
|
||||
} finally {
|
||||
healthLoading.value = false;
|
||||
}
|
||||
return config ? config.value : '';
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
await getEnabled();
|
||||
await getModuleConfig();
|
||||
await Promise.all([getModuleConfig(), refreshHealth()]);
|
||||
};
|
||||
|
||||
const onClickTestConnection = async () => {
|
||||
const workspaceId = getModuleConfigValue('workplaceId') || null;
|
||||
const channelId = getModuleConfigValue('channelId') || null;
|
||||
|
||||
SessionUser.superUser.modules.bird.functions.testOutboundVoiceCall(workspaceId, channelId).then((response) => {
|
||||
console.log(response);
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Connection successful',
|
||||
text: 'The connection to Bird API was successful.',
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.log(error);
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Connection failed',
|
||||
text: 'The connection to Bird API failed.',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
load();
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RestrictedPageWrapper :hasPermission="SessionUser.canAccessSuperUser()">
|
||||
<ConfigurationSubPageWrapper>
|
||||
<template #title>
|
||||
<PageTitle title="Bird configuration" subtitle="The configuration of the Bird integration" />
|
||||
<PageTitle
|
||||
:title="$t('configuration.bird.title')"
|
||||
:subtitle="$t('configuration.bird.subtitle')"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="module_config.length > 0" #content>
|
||||
<ConfigurationCategory
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
title="General settings"
|
||||
description="The general settings for the Bird integration."
|
||||
icon="fas fa-cogs"
|
||||
>
|
||||
<ConfigurationSwitch
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
title="Enable Bird"
|
||||
description="Enable or disable the Bird integration."
|
||||
icon="fas fa-cogs"
|
||||
:value="getModuleConfigValue('enabled') === true"
|
||||
:on-switch="SessionUser.superUser.modules.bird.config.enabled.set"
|
||||
/>
|
||||
</ConfigurationCategory>
|
||||
<ConfigurationCategory
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
title="API connection settings"
|
||||
description="The Bird API connection settings."
|
||||
icon="fas fa-cogs"
|
||||
>
|
||||
<ConfigurationSecretKey
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
title="API key"
|
||||
description="The API key for the Bird integration."
|
||||
icon="fas fa-key"
|
||||
:isSet="getModuleConfigValue('api_key') !== ''"
|
||||
:on-save="SessionUser.superUser.modules.bird.config.keys.api_key.set"
|
||||
/>
|
||||
<ConfigurationInput
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
title="Server URL"
|
||||
description="The Bird API server URL."
|
||||
icon="fas fa-link"
|
||||
:value="getModuleConfigValue('server_url')"
|
||||
:on-save="SessionUser.superUser.modules.bird.config.keys.server_url.set"
|
||||
/>
|
||||
<ConfigurationInput
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
title="Workplace ID"
|
||||
description="The Bird workplace ID used for voice call APIs."
|
||||
icon="fas fa-building"
|
||||
:value="getModuleConfigValue('workplaceId')"
|
||||
:on-save="SessionUser.superUser.modules.bird.config.keys.workplaceId.set"
|
||||
/>
|
||||
<ConfigurationInput
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
title="Channel ID"
|
||||
description="The Bird channel ID used for voice call APIs."
|
||||
icon="fas fa-broadcast-tower"
|
||||
:value="getModuleConfigValue('channelId')"
|
||||
:on-save="SessionUser.superUser.modules.bird.config.keys.channelId.set"
|
||||
/>
|
||||
</ConfigurationCategory>
|
||||
<div class="buttons">
|
||||
<button class="button is-dark" @click="onClickTestConnection">
|
||||
<span class="icon">
|
||||
<i class="fas fa-sync"></i>
|
||||
</span>
|
||||
<span>Test connection</span>
|
||||
</button>
|
||||
|
||||
<template #content>
|
||||
<div v-if="configUnavailable" class="message is-danger" data-testid="bird-config-unavailable">
|
||||
<div class="message-body">{{ $t("configuration.bird.config_unavailable") }}</div>
|
||||
</div>
|
||||
|
||||
<ConfigurationCategory
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.health_title')"
|
||||
:subtitle="$t(`configuration.bird.state.${healthState}`)"
|
||||
icon="fas fa-heartbeat"
|
||||
:default-expanded="true"
|
||||
>
|
||||
<div class="bird-health">
|
||||
<div>
|
||||
<span
|
||||
class="tag is-medium"
|
||||
:class="healthStateClass"
|
||||
data-testid="bird-health-state"
|
||||
>
|
||||
{{ $t(`configuration.bird.state.${healthState}`) }}
|
||||
</span>
|
||||
<p class="help mt-2">{{ $t("configuration.bird.read_only_health") }}</p>
|
||||
</div>
|
||||
|
||||
<dl class="bird-health__facts">
|
||||
<div>
|
||||
<dt>{{ $t("configuration.bird.workspace_id") }}</dt>
|
||||
<dd>{{ health.workspaceId || configuredWorkspaceId || $t("configuration.bird.not_configured") }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ $t("configuration.bird.channel_id") }}</dt>
|
||||
<dd>{{ health.channelId || getModuleConfigValue("channelId") || $t("configuration.bird.not_configured") }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ $t("configuration.bird.provider_check") }}</dt>
|
||||
<dd>{{ health.check || $t("configuration.bird.not_available") }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<ul v-if="normalizedChannels.length > 0" class="bird-channels">
|
||||
<li
|
||||
v-for="(channel, channelIndex) in normalizedChannels"
|
||||
:key="channel.id || channel.channelId || channel.name || channelIndex"
|
||||
class="bird-channels__item"
|
||||
>
|
||||
<span>{{ channel.name || channel.id || channel.channelId || $t("configuration.bird.unnamed_channel") }}</span>
|
||||
<span v-if="channel.platform" class="tag is-light">{{ channel.platform }}</span>
|
||||
<span v-if="channel.status" class="tag is-light">{{ channel.status }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button
|
||||
class="button is-dark mt-3"
|
||||
:class="{ 'is-loading': healthLoading }"
|
||||
:disabled="healthLoading"
|
||||
data-testid="bird-refresh-health"
|
||||
@click="refreshHealth"
|
||||
>
|
||||
<span class="icon"><i class="fas fa-sync"></i></span>
|
||||
<span>{{ $t("configuration.bird.refresh_health") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</ConfigurationCategory>
|
||||
|
||||
<template v-if="!configLoading && moduleConfig.length > 0">
|
||||
<ConfigurationCategory
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.general_settings')"
|
||||
:subtitle="$t('configuration.bird.general_settings_desc')"
|
||||
icon="fas fa-cogs"
|
||||
>
|
||||
<ConfigurationSwitch
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.enable')"
|
||||
:description="$t('configuration.bird.enable_desc')"
|
||||
icon="fas fa-power-off"
|
||||
:value="getModuleConfigValue('enabled') === true"
|
||||
:on-switch="SessionUser.superUser.modules.bird.config.enabled.set"
|
||||
/>
|
||||
</ConfigurationCategory>
|
||||
|
||||
<ConfigurationCategory
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.api_settings')"
|
||||
:subtitle="$t('configuration.bird.api_settings_desc')"
|
||||
icon="fas fa-plug"
|
||||
>
|
||||
<ConfigurationSecretKey
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.api_key')"
|
||||
:description="$t('configuration.bird.api_key_desc')"
|
||||
icon="fas fa-key"
|
||||
:isSet="isModuleSecretSet('api_key')"
|
||||
:on-save="SessionUser.superUser.modules.bird.config.keys.api_key.set"
|
||||
/>
|
||||
<ConfigurationInput
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.server_url')"
|
||||
:description="$t('configuration.bird.server_url_desc')"
|
||||
icon="fas fa-link"
|
||||
:value="getModuleConfigValue('server_url')"
|
||||
:on-save="SessionUser.superUser.modules.bird.config.keys.server_url.set"
|
||||
/>
|
||||
<ConfigurationInput
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.workspace_id')"
|
||||
:description="$t('configuration.bird.workspace_id_desc')"
|
||||
icon="fas fa-building"
|
||||
:value="configuredWorkspaceId"
|
||||
:on-save="SessionUser.superUser.modules.bird.config.keys.workspaceId.set"
|
||||
/>
|
||||
<ConfigurationInput
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.channel_id')"
|
||||
:description="$t('configuration.bird.channel_id_desc')"
|
||||
icon="fas fa-broadcast-tower"
|
||||
:value="getModuleConfigValue('channelId')"
|
||||
:on-save="SessionUser.superUser.modules.bird.config.keys.channelId.set"
|
||||
/>
|
||||
<ConfigurationInput
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.participant_id')"
|
||||
:description="$t('configuration.bird.participant_id_desc')"
|
||||
icon="fas fa-user-tag"
|
||||
:value="getModuleConfigValue('participantId')"
|
||||
:on-save="SessionUser.superUser.modules.bird.config.keys.participantId.set"
|
||||
/>
|
||||
<ConfigurationInput
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.allowed_channels')"
|
||||
:description="$t('configuration.bird.allowed_channels_desc')"
|
||||
icon="fas fa-list-check"
|
||||
:value="getModuleConfigValue('allowed_channel_ids_json')"
|
||||
:on-save="SessionUser.superUser.modules.bird.config.keys.allowed_channel_ids_json.set"
|
||||
/>
|
||||
</ConfigurationCategory>
|
||||
|
||||
<ConfigurationCategory
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.control_plane')"
|
||||
:subtitle="$t('configuration.bird.control_plane_desc')"
|
||||
icon="fas fa-shield-halved"
|
||||
>
|
||||
<ConfigurationSwitch
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.control_plane_enable')"
|
||||
:description="$t('configuration.bird.control_plane_enable_desc')"
|
||||
icon="fas fa-network-wired"
|
||||
:value="getModuleConfigValue('control_plane_enabled') === true"
|
||||
:on-switch="SessionUser.superUser.modules.bird.config.keys.control_plane_enabled.set"
|
||||
/>
|
||||
<ConfigurationSecretKey
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.control_plane_token')"
|
||||
:description="$t('configuration.bird.control_plane_token_desc')"
|
||||
icon="fas fa-key"
|
||||
:isSet="isModuleSecretSet('control_plane_token')"
|
||||
:on-save="SessionUser.superUser.modules.bird.config.keys.control_plane_token.set"
|
||||
/>
|
||||
<ConfigurationInput
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.webhook_url')"
|
||||
:description="$t('configuration.bird.webhook_url_desc')"
|
||||
icon="fas fa-link"
|
||||
:value="getModuleConfigValue('webhook_public_url')"
|
||||
:on-save="SessionUser.superUser.modules.bird.config.keys.webhook_public_url.set"
|
||||
/>
|
||||
<ConfigurationSecretKey
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.webhook_key')"
|
||||
:description="$t('configuration.bird.webhook_key_desc')"
|
||||
icon="fas fa-signature"
|
||||
:isSet="isModuleSecretSet('webhook_signing_key')"
|
||||
:on-save="SessionUser.superUser.modules.bird.config.keys.webhook_signing_key.set"
|
||||
/>
|
||||
<ConfigurationInput
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.replay_window')"
|
||||
:description="$t('configuration.bird.replay_window_desc')"
|
||||
icon="fas fa-clock"
|
||||
:value="getModuleConfigValue('webhook_replay_window_seconds')"
|
||||
:on-save="SessionUser.superUser.modules.bird.config.keys.webhook_replay_window_seconds.set"
|
||||
/>
|
||||
</ConfigurationCategory>
|
||||
|
||||
<ConfigurationCategory
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.automation')"
|
||||
:subtitle="$t('configuration.bird.automation_desc')"
|
||||
icon="fas fa-diagram-project"
|
||||
>
|
||||
<ConfigurationSwitch
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.flow_enable')"
|
||||
:description="$t('configuration.bird.flow_enable_desc')"
|
||||
icon="fas fa-code-branch"
|
||||
:value="getModuleConfigValue('flow_enabled') === true"
|
||||
:on-switch="SessionUser.superUser.modules.bird.config.keys.flow_enabled.set"
|
||||
/>
|
||||
<ConfigurationSecretKey
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.flow_secret')"
|
||||
:description="$t('configuration.bird.flow_secret_desc')"
|
||||
icon="fas fa-key"
|
||||
:isSet="isModuleSecretSet('flow_shared_secret')"
|
||||
:on-save="SessionUser.superUser.modules.bird.config.keys.flow_shared_secret.set"
|
||||
/>
|
||||
<ConfigurationInput
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.flow_policy')"
|
||||
:description="$t('configuration.bird.flow_policy_desc')"
|
||||
icon="fas fa-code"
|
||||
:value="getModuleConfigValue('flow_policy_json')"
|
||||
:on-save="SessionUser.superUser.modules.bird.config.keys.flow_policy_json.set"
|
||||
/>
|
||||
<ConfigurationSwitch
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.operations_enable')"
|
||||
:description="$t('configuration.bird.operations_enable_desc')"
|
||||
icon="fas fa-phone-slash"
|
||||
:value="getModuleConfigValue('operations_actions_enabled') === true"
|
||||
:on-switch="SessionUser.superUser.modules.bird.config.keys.operations_actions_enabled.set"
|
||||
/>
|
||||
<ConfigurationSwitch
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.outbound_enable')"
|
||||
:description="$t('configuration.bird.outbound_enable_desc')"
|
||||
icon="fas fa-paper-plane"
|
||||
:value="getModuleConfigValue('outbound_messages_enabled') === true"
|
||||
:on-switch="SessionUser.superUser.modules.bird.config.keys.outbound_messages_enabled.set"
|
||||
/>
|
||||
<ConfigurationInput
|
||||
class="mt-2"
|
||||
module="Bird"
|
||||
:title="$t('configuration.bird.template_policy')"
|
||||
:description="$t('configuration.bird.template_policy_desc')"
|
||||
icon="fas fa-file-code"
|
||||
:value="getModuleConfigValue('template_policy_json')"
|
||||
:on-save="SessionUser.superUser.modules.bird.config.keys.template_policy_json.set"
|
||||
/>
|
||||
</ConfigurationCategory>
|
||||
</template>
|
||||
</template>
|
||||
</ConfigurationSubPageWrapper>
|
||||
</RestrictedPageWrapper>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bird-health {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.bird-health__facts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bird-health__facts > div {
|
||||
border: 1px solid hsl(0deg 0% 86%);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.bird-health__facts dt {
|
||||
color: hsl(0deg 0% 48%);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.bird-health__facts dd {
|
||||
margin: 0.25rem 0 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.bird-channels {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bird-channels__item {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { flushPromises, mount } from "@vue/test-utils";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const birdMocks = vi.hoisted(() => ({
|
||||
getAll: vi.fn(),
|
||||
getStatus: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/displays/superuser/configuration/ConfigurationCategory.vue", () => ({
|
||||
default: {
|
||||
props: ["title", "subtitle"],
|
||||
template: '<section class="configuration-category"><h2>{{ title }}</h2><p>{{ subtitle }}</p><slot /></section>',
|
||||
},
|
||||
}));
|
||||
vi.mock("@/components/displays/superuser/configuration/ConfigurationInput.vue", () => ({
|
||||
default: {
|
||||
props: ["title", "value"],
|
||||
template: '<div class="configuration-input">{{ title }}:{{ value }}</div>',
|
||||
},
|
||||
}));
|
||||
vi.mock("@/components/displays/superuser/configuration/ConfigurationSecretKey.vue", () => ({
|
||||
default: {
|
||||
props: ["isSet"],
|
||||
template: '<div data-testid="bird-secret-state">{{ isSet ? "set" : "unset" }}</div>',
|
||||
},
|
||||
}));
|
||||
vi.mock("@/components/displays/superuser/configuration/ConfigurationSwitch.vue", () => ({
|
||||
default: { template: "<div />" },
|
||||
}));
|
||||
vi.mock("@/components/global/PageTitle.vue", () => ({
|
||||
default: { template: "<header />" },
|
||||
}));
|
||||
vi.mock("@/components/page/wrappers/RestrictedPageWrapper.vue", () => ({
|
||||
default: { template: "<div><slot /></div>" },
|
||||
}));
|
||||
vi.mock("@/views/dashboards/superUserDashboard/configuration/ConfigurationSubPageWrapper.vue", () => ({
|
||||
default: { template: "<main><slot name=\"title\" /><slot name=\"content\" /></main>" },
|
||||
}));
|
||||
|
||||
vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
||||
SessionUser: {
|
||||
canAccessSuperUser: () => true,
|
||||
superUser: {
|
||||
modules: {
|
||||
bird: {
|
||||
config: {
|
||||
get_all: birdMocks.getAll,
|
||||
enabled: { set: vi.fn() },
|
||||
keys: {
|
||||
api_key: { set: vi.fn() },
|
||||
server_url: { set: vi.fn() },
|
||||
workspaceId: { set: vi.fn() },
|
||||
channelId: { set: vi.fn() },
|
||||
participantId: { set: vi.fn() },
|
||||
allowed_channel_ids_json: { set: vi.fn() },
|
||||
control_plane_enabled: { set: vi.fn() },
|
||||
control_plane_token: { set: vi.fn() },
|
||||
webhook_signing_key: { set: vi.fn() },
|
||||
webhook_public_url: { set: vi.fn() },
|
||||
webhook_replay_window_seconds: { set: vi.fn() },
|
||||
flow_enabled: { set: vi.fn() },
|
||||
flow_shared_secret: { set: vi.fn() },
|
||||
flow_policy_json: { set: vi.fn() },
|
||||
operations_actions_enabled: { set: vi.fn() },
|
||||
outbound_messages_enabled: { set: vi.fn() },
|
||||
template_policy_json: { set: vi.fn() },
|
||||
},
|
||||
},
|
||||
functions: {
|
||||
getHealth: birdMocks.getStatus,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import ConfigurationBird from "@/views/dashboards/superUserDashboard/configuration/ConfigurationBird.vue";
|
||||
|
||||
const global = {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
};
|
||||
|
||||
describe("Bird configuration", () => {
|
||||
beforeEach(() => {
|
||||
birdMocks.getAll.mockReset();
|
||||
birdMocks.getStatus.mockReset();
|
||||
});
|
||||
|
||||
it("renders healthy read-only status and provider channel state", async () => {
|
||||
birdMocks.getAll.mockResolvedValue({
|
||||
data: {
|
||||
data: {
|
||||
enabled: { variable: "enabled", value: true },
|
||||
workspaceId: { variable: "workspaceId", value: "workspace-current" },
|
||||
},
|
||||
},
|
||||
});
|
||||
birdMocks.getStatus.mockResolvedValue({
|
||||
data: {
|
||||
data: {
|
||||
enabled: true,
|
||||
configured: true,
|
||||
providerReachable: true,
|
||||
workspaceId: "workspace-current",
|
||||
channelId: "channel-1",
|
||||
check: "channels.list",
|
||||
channels: [{ id: "channel-1", name: "Customer WhatsApp", platform: "whatsapp", status: "active" }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mount(ConfigurationBird, { global });
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.get('[data-testid="bird-health-state"]').text()).toBe("configuration.bird.state.healthy");
|
||||
expect(wrapper.text()).toContain("configuration.bird.read_only_health");
|
||||
expect(wrapper.text()).toContain("channels.list");
|
||||
expect(wrapper.text()).toContain("Customer WhatsApp");
|
||||
expect(wrapper.text()).toContain("whatsapp");
|
||||
expect(wrapper.text()).toContain("active");
|
||||
});
|
||||
|
||||
it("uses workplaceId only as a display fallback while exposing the canonical field", async () => {
|
||||
birdMocks.getAll.mockResolvedValue({
|
||||
data: {
|
||||
data: {
|
||||
enabled: { variable: "enabled", value: false },
|
||||
workplaceId: { variable: "workplaceId", value: "workspace-legacy" },
|
||||
},
|
||||
},
|
||||
});
|
||||
birdMocks.getStatus.mockResolvedValue({
|
||||
data: {
|
||||
data: {
|
||||
enabled: false,
|
||||
configured: false,
|
||||
healthy: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mount(ConfigurationBird, { global });
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.get('[data-testid="bird-health-state"]').text()).toBe("configuration.bird.state.disabled");
|
||||
expect(wrapper.text()).toContain("configuration.bird.workspace_id:workspace-legacy");
|
||||
});
|
||||
|
||||
it("fails closed to an unavailable health state", async () => {
|
||||
birdMocks.getAll.mockRejectedValue(new Error("Config unavailable"));
|
||||
birdMocks.getStatus.mockRejectedValue(new Error("Health unavailable"));
|
||||
|
||||
const wrapper = mount(ConfigurationBird, { global });
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.get('[data-testid="bird-config-unavailable"]').text()).toBe(
|
||||
"configuration.bird.config_unavailable"
|
||||
);
|
||||
expect(wrapper.get('[data-testid="bird-health-state"]').text()).toBe("configuration.bird.state.unavailable");
|
||||
});
|
||||
|
||||
it("uses redacted secret metadata and never renders an accidentally returned secret value", async () => {
|
||||
birdMocks.getAll.mockResolvedValue({
|
||||
data: {
|
||||
data: {
|
||||
enabled: { variable: "enabled", value: true },
|
||||
api_key: {
|
||||
variable: "api_key",
|
||||
value: "must-not-reach-the-dom",
|
||||
isSecret: true,
|
||||
isSet: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
birdMocks.getStatus.mockResolvedValue({
|
||||
data: {
|
||||
data: {
|
||||
enabled: true,
|
||||
configured: true,
|
||||
healthy: true,
|
||||
channels: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mount(ConfigurationBird, { global });
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.get('[data-testid="bird-secret-state"]').text()).toBe("set");
|
||||
expect(wrapper.text()).not.toContain("must-not-reach-the-dom");
|
||||
});
|
||||
|
||||
it("redacts the legacy API key even before isSecret metadata is deployed", async () => {
|
||||
birdMocks.getAll.mockResolvedValue({
|
||||
data: {
|
||||
data: {
|
||||
enabled: { variable: "enabled", value: true },
|
||||
api_key: { variable: "api_key", value: "legacy-raw-secret" },
|
||||
},
|
||||
},
|
||||
});
|
||||
birdMocks.getStatus.mockResolvedValue({
|
||||
data: { data: { enabled: true, configured: true, healthy: true, channels: [] } },
|
||||
});
|
||||
|
||||
const wrapper = mount(ConfigurationBird, { global });
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.get('[data-testid="bird-secret-state"]').text()).toBe("set");
|
||||
expect(wrapper.text()).not.toContain("legacy-raw-secret");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const authenticatedRequestMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("@/components/session/authenticatedRequest.vue", () => ({
|
||||
authenticatedRequest: authenticatedRequestMock,
|
||||
}));
|
||||
|
||||
import { Bird } from "@/components/session/token/superUser/modules/bird/Bird.vue";
|
||||
import { Config } from "@/components/session/token/superUser/modules/bird/Config.vue";
|
||||
|
||||
const root = process.cwd();
|
||||
const pageSource = readFileSync(
|
||||
join(root, "src/views/dashboards/superUserDashboard/configuration/ConfigurationBird.vue"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
describe("Bird module contract", () => {
|
||||
beforeEach(() => {
|
||||
authenticatedRequestMock.mockReset();
|
||||
authenticatedRequestMock.mockResolvedValue({ data: { data: {} } });
|
||||
});
|
||||
|
||||
it("uses the end-user read-only health endpoint for configuration health", async () => {
|
||||
await Bird.functions.getHealth();
|
||||
|
||||
expect(authenticatedRequestMock).toHaveBeenCalledOnce();
|
||||
expect(authenticatedRequestMock).toHaveBeenCalledWith("/bird/health", "GET");
|
||||
});
|
||||
|
||||
it("writes the canonical workspaceId while retaining the legacy workplaceId adapter", async () => {
|
||||
await Config.keys.workspaceId.set("workspace-canonical");
|
||||
await Config.keys.workplaceId.get();
|
||||
|
||||
expect(authenticatedRequestMock).toHaveBeenNthCalledWith(1, "/bird/config", "POST", {
|
||||
variable: "workspaceId",
|
||||
value: "workspace-canonical",
|
||||
});
|
||||
expect(authenticatedRequestMock).toHaveBeenNthCalledWith(2, "/bird/config?variable=workplaceId", "GET");
|
||||
});
|
||||
|
||||
it("exposes protected gateway and channel configuration through typed config keys", async () => {
|
||||
await Config.keys.allowed_channel_ids_json.set('["channel-1"]');
|
||||
await Config.keys.control_plane_enabled.set(true);
|
||||
await Config.keys.webhook_signing_key.set("rotated-secret");
|
||||
await Config.keys.participantId.set("participant-1");
|
||||
|
||||
expect(authenticatedRequestMock).toHaveBeenNthCalledWith(1, "/bird/config", "POST", {
|
||||
variable: "allowed_channel_ids_json",
|
||||
value: '["channel-1"]',
|
||||
});
|
||||
expect(authenticatedRequestMock).toHaveBeenNthCalledWith(2, "/bird/config", "POST", {
|
||||
variable: "control_plane_enabled",
|
||||
value: true,
|
||||
});
|
||||
expect(authenticatedRequestMock).toHaveBeenNthCalledWith(3, "/bird/config", "POST", {
|
||||
variable: "webhook_signing_key",
|
||||
value: "rotated-secret",
|
||||
});
|
||||
expect(authenticatedRequestMock).toHaveBeenNthCalledWith(4, "/bird/config", "POST", {
|
||||
variable: "participantId",
|
||||
value: "participant-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not place a fixed-number call from the configuration page", () => {
|
||||
expect(pageSource).toContain("getHealth");
|
||||
expect(pageSource).not.toContain("getControlPlaneStatus");
|
||||
expect(pageSource).toContain("getModuleConfigValue(\"workspaceId\") || getModuleConfigValue(\"workplaceId\")");
|
||||
expect(pageSource).toContain("config.keys.workspaceId.set");
|
||||
expect(pageSource).toContain("BIRD_SECRET_VARIABLES.has(value.variable)");
|
||||
expect(pageSource).toContain("isModuleSecretSet('api_key')");
|
||||
expect(pageSource).not.toContain("testOutboundVoiceCall");
|
||||
expect(pageSource).not.toContain("console.");
|
||||
});
|
||||
|
||||
it("renders health, disabled, channel, and read-only states through i18n keys", () => {
|
||||
expect(pageSource).toContain("configuration.bird.health_title");
|
||||
expect(pageSource).toContain("configuration.bird.read_only_health");
|
||||
expect(pageSource).toContain("configuration.bird.state.");
|
||||
expect(pageSource).toContain("normalizedChannels");
|
||||
expect(pageSource).toContain('data-testid="bird-health-state"');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user