mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-08-19 09:30:21 +00:00
fix(ui): keep the Slack page usable on an unreadable check time
- Guard the last-checked timestamp with isValid before formatting - Fall back to the never-checked rendering instead of the error boundary
This commit is contained in:
@@ -193,6 +193,17 @@ export const connectedSlackFixture = (
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const configuredInstall = (): SlackInstallFixture => ({
|
||||
id: SLACK_INTEGRATION_ID,
|
||||
connected: true,
|
||||
connectionLastCheckedAt: "2026-08-10T09:30:00Z",
|
||||
workspace: {
|
||||
...PROWLER_HQ,
|
||||
channelId: SLACK_DEFAULT_CHANNEL.id,
|
||||
channelName: SLACK_DEFAULT_CHANNEL.name,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* A workspace connected *and* a channel on record. Anything the API refuses
|
||||
* until a channel exists (the connection check) needs this fixture.
|
||||
@@ -200,16 +211,17 @@ export const connectedSlackFixture = (
|
||||
export const configuredSlackFixture = (
|
||||
overrides: Partial<SlackFixture> = {},
|
||||
): SlackFixture =>
|
||||
connectedSlackFixture({ install: configuredInstall(), ...overrides });
|
||||
|
||||
/**
|
||||
* The same finished setup, with a check time no parser can read: a zero date
|
||||
* from a bad write or a serializer change. The contract types the attribute as
|
||||
* a string and rules nothing else out.
|
||||
*/
|
||||
export const unreadableCheckTimeSlackFixture = (): SlackFixture =>
|
||||
connectedSlackFixture({
|
||||
install: {
|
||||
id: SLACK_INTEGRATION_ID,
|
||||
connected: true,
|
||||
connectionLastCheckedAt: "2026-08-10T09:30:00Z",
|
||||
workspace: {
|
||||
...PROWLER_HQ,
|
||||
channelId: SLACK_DEFAULT_CHANNEL.id,
|
||||
channelName: SLACK_DEFAULT_CHANNEL.name,
|
||||
},
|
||||
...configuredInstall(),
|
||||
connectionLastCheckedAt: "0000-00-00T00:00:00Z",
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
@@ -240,6 +240,17 @@ export class SlackIntegrationHarness extends BrowserHarness<SlackFixture> {
|
||||
return this.containsText(/Choosing a destination channel is the next step/);
|
||||
}
|
||||
|
||||
/**
|
||||
* The "last checked" line as rendered, or null when the page shows none —
|
||||
* which is what a workspace whose connection was never checked shows.
|
||||
*/
|
||||
lastCheckedLine(): string | null {
|
||||
const line = Array.from(
|
||||
this.container.querySelectorAll<HTMLElement>("p"),
|
||||
).find((p) => /^Last checked:/.test((p.textContent ?? "").trim()));
|
||||
return line ? (line.textContent ?? "").trim() : null;
|
||||
}
|
||||
|
||||
async testConnection(): Promise<ConnectionOutcome> {
|
||||
await this.clickButton(/Test connection/);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
connectedSlackFixture,
|
||||
INTEGRATIONS_SERVER_ERROR_DETAIL,
|
||||
slackFixture,
|
||||
unreadableCheckTimeSlackFixture,
|
||||
} from "@/__tests__/msw/handlers/slack.fixtures";
|
||||
|
||||
import {
|
||||
@@ -190,6 +191,23 @@ describe("a connected workspace", () => {
|
||||
expect(badge).not.toMatch(/Disconnected/);
|
||||
}, 30000);
|
||||
|
||||
it("keeps the page usable when the recorded check time is one no parser can read", async () => {
|
||||
// Given — a finished setup whose `connection_last_checked_at` is a zero
|
||||
// date. `date-fns` throws a RangeError on it, which would replace the whole
|
||||
// page with the route's error boundary.
|
||||
const harness = new SlackIntegrationHarness(
|
||||
unreadableCheckTimeSlackFixture(),
|
||||
);
|
||||
|
||||
await harness.mount();
|
||||
|
||||
expect(await harness.connectedWorkspaceName()).toBe(WORKSPACE_NAME);
|
||||
expect(await harness.connectionBadge()).toBe("Connected");
|
||||
// Nothing to show, so nothing is shown: the same line a workspace that was
|
||||
// never checked renders.
|
||||
expect(harness.lastCheckedLine()).toBeNull();
|
||||
}, 30000);
|
||||
|
||||
it("does not offer a connection check the API is bound to refuse", async () => {
|
||||
// Given — a workspace connected and no destination channel recorded.
|
||||
const harness = new SlackIntegrationHarness(connectedSlackFixture());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { format } from "date-fns";
|
||||
import { format, isValid, parseISO } from "date-fns";
|
||||
import { TestTube } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -73,6 +73,14 @@ export const SlackIntegrationManager = ({
|
||||
// Absent until a channel is chosen, never present-and-null.
|
||||
const channelId = configuration?.channel_id ?? null;
|
||||
|
||||
const checkedAt = integration?.attributes.connection_last_checked_at;
|
||||
const checkedOn = checkedAt ? parseISO(checkedAt) : null;
|
||||
// `format` throws a RangeError on an unreadable value, which would replace
|
||||
// the page with the route's error boundary: show nothing instead, as for a
|
||||
// connection that was never checked.
|
||||
const lastCheckedOn =
|
||||
checkedOn && isValid(checkedOn) ? format(checkedOn, "yyyy/MM/dd") : null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{rateLimitMessage && (
|
||||
@@ -118,15 +126,10 @@ export const SlackIntegrationManager = ({
|
||||
<CardContent className="pt-0">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="text-xs text-gray-500 dark:text-gray-300">
|
||||
{integration.attributes.connection_last_checked_at && (
|
||||
{lastCheckedOn && (
|
||||
<p>
|
||||
<span className="font-medium">Last checked:</span>{" "}
|
||||
{format(
|
||||
new Date(
|
||||
integration.attributes.connection_last_checked_at,
|
||||
),
|
||||
"yyyy/MM/dd",
|
||||
)}
|
||||
{lastCheckedOn}
|
||||
</p>
|
||||
)}
|
||||
{!channelId && (
|
||||
|
||||
Reference in New Issue
Block a user