1
0
forked from baron/baron-sso

ci: add code check badges and coverage reports

This commit is contained in:
2026-05-29 12:05:43 +09:00
parent c489c7c38f
commit a830242947
164 changed files with 9059 additions and 2012 deletions

View File

@@ -1,5 +1,5 @@
import { act } from "react";
import { type Root, createRoot } from "react-dom/client";
import { createRoot, type Root } from "react-dom/client";
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
import { afterEach, describe, expect, it, vi } from "vitest";
import AuthGuard from "./AuthGuard";

View File

@@ -1,5 +1,5 @@
import { act } from "react";
import { type Root, createRoot } from "react-dom/client";
import { createRoot, type Root } from "react-dom/client";
import { MemoryRouter } from "react-router-dom";
import { afterEach, describe, expect, it, vi } from "vitest";
import LoginPage from "./LoginPage";

View File

@@ -1,8 +1,7 @@
import { ExternalLink, LogIn, ShieldHalf } from "lucide-react";
import { useEffect, useRef } from "react";
import { useAuth } from "react-oidc-context";
import { useNavigate } from "react-router-dom";
import { useSearchParams } from "react-router-dom";
import { useNavigate, useSearchParams } from "react-router-dom";
import { Button } from "../../components/ui/button";
import {
Card,

View File

@@ -27,6 +27,11 @@ import { Label } from "../../components/ui/label";
import { Switch } from "../../components/ui/switch";
import { Textarea } from "../../components/ui/textarea";
import { toast } from "../../components/ui/use-toast";
import type {
ClientStatus,
ClientType,
ClientUpsertRequest,
} from "../../lib/devApi";
import {
createClient,
deleteClient,
@@ -36,11 +41,6 @@ import {
updateClient,
updateClientStatus,
} from "../../lib/devApi";
import type {
ClientStatus,
ClientType,
ClientUpsertRequest,
} from "../../lib/devApi";
import { t } from "../../lib/i18n";
import { cn } from "../../lib/utils";
@@ -1118,7 +1118,7 @@ function ClientGeneralPage() {
</span>
{securityProfile === "pkce" && (
<div
<fieldset
className="mt-4 pt-4 border-t border-primary/20 flex items-center justify-between"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
@@ -1145,7 +1145,7 @@ function ClientGeneralPage() {
checked={headlessLoginEnabled}
onCheckedChange={handleHeadlessToggle}
/>
</div>
</fieldset>
)}
</label>
</div>
@@ -1454,104 +1454,102 @@ function ClientGeneralPage() {
</div>
{currentHeadlessJwksCache.parsedKeys?.length ? (
<div className="space-y-3">
{currentHeadlessJwksCache.parsedKeys.map(
(key, index) => {
const normalizedAlgorithm = key.alg?.trim() ?? "";
const isMissingAlgorithm =
normalizedAlgorithm === "";
const isUnsupportedAlgorithm =
!isMissingAlgorithm &&
!HEADLESS_LOGIN_ALLOWED_ALGORITHM_SET.has(
normalizedAlgorithm,
);
{currentHeadlessJwksCache.parsedKeys.map((key) => {
const normalizedAlgorithm = key.alg?.trim() ?? "";
const isMissingAlgorithm =
normalizedAlgorithm === "";
const isUnsupportedAlgorithm =
!isMissingAlgorithm &&
!HEADLESS_LOGIN_ALLOWED_ALGORITHM_SET.has(
normalizedAlgorithm,
);
return (
<div
key={`${key.kid || "key"}-${index}`}
className={cn(
"rounded-xl border bg-muted/30 p-3",
isUnsupportedAlgorithm || isMissingAlgorithm
? "border-destructive/50 bg-destructive/5"
: "border-border",
)}
>
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<div className="space-y-1">
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
KID
</p>
<p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]">
{key.kid || "-"}
</p>
</div>
<div className="space-y-1">
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
KTY
</p>
<p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]">
{key.kty || "-"}
</p>
</div>
<div className="space-y-1">
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
USE
</p>
<p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]">
{key.use || "-"}
</p>
</div>
<div className="space-y-1">
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
ALG
</p>
<p
className={cn(
"break-all rounded-lg border bg-background px-3 py-2 font-mono text-[11px]",
isUnsupportedAlgorithm ||
isMissingAlgorithm
? "border-destructive/50 text-destructive"
: "border-border",
)}
>
{key.alg ||
t(
"msg.dev.clients.general.public_key.cache.missing_algorithm_badge",
"알고리즘 미선언",
)}
</p>
{isMissingAlgorithm && (
<p className="text-[11px] text-destructive">
{t(
"msg.dev.clients.general.public_key.cache.missing_algorithm_reason",
"이 키는 `alg`가 비어 있어서 저장할 수 없습니다.",
)}
</p>
)}
{isUnsupportedAlgorithm && (
<p className="text-[11px] text-destructive">
{t(
"msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason",
"이 알고리즘은 Headless Login에서 지원되지 않습니다.",
)}
</p>
)}
</div>
</div>
<div className="mt-3 space-y-1">
return (
<div
key={`${key.kid ?? "missing-kid"}-${key.kty ?? ""}-${key.alg ?? ""}-${key.n ?? ""}`}
className={cn(
"rounded-xl border bg-muted/30 p-3",
isUnsupportedAlgorithm || isMissingAlgorithm
? "border-destructive/50 bg-destructive/5"
: "border-border",
)}
>
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<div className="space-y-1">
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
{t(
"ui.dev.clients.general.public_key.cache.parsed_key_n",
"N",
KID
</p>
<p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]">
{key.kid || "-"}
</p>
</div>
<div className="space-y-1">
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
KTY
</p>
<p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]">
{key.kty || "-"}
</p>
</div>
<div className="space-y-1">
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
USE
</p>
<p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]">
{key.use || "-"}
</p>
</div>
<div className="space-y-1">
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
ALG
</p>
<p
className={cn(
"break-all rounded-lg border bg-background px-3 py-2 font-mono text-[11px]",
isUnsupportedAlgorithm ||
isMissingAlgorithm
? "border-destructive/50 text-destructive"
: "border-border",
)}
>
{key.alg ||
t(
"msg.dev.clients.general.public_key.cache.missing_algorithm_badge",
"알고리즘 미선언",
)}
</p>
<p className="min-h-16 break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px] leading-5">
{key.n || "-"}
</p>
{isMissingAlgorithm && (
<p className="text-[11px] text-destructive">
{t(
"msg.dev.clients.general.public_key.cache.missing_algorithm_reason",
"이 키는 `alg`가 비어 있어서 저장할 수 없습니다.",
)}
</p>
)}
{isUnsupportedAlgorithm && (
<p className="text-[11px] text-destructive">
{t(
"msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason",
"이 알고리즘은 Headless Login에서 지원되지 않습니다.",
)}
</p>
)}
</div>
</div>
);
},
)}
<div className="mt-3 space-y-1">
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
{t(
"ui.dev.clients.general.public_key.cache.parsed_key_n",
"N",
)}
</p>
<p className="min-h-16 break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px] leading-5">
{key.n || "-"}
</p>
</div>
</div>
);
})}
</div>
) : (
<div className="rounded-lg border border-dashed border-border px-4 py-5 text-sm text-muted-foreground">

View File

@@ -19,11 +19,11 @@ import {
TableHeader,
TableRow,
} from "../../../components/ui/table";
import type { IdpConfig, IdpConfigCreateRequest } from "../../../lib/devApi";
import {
createIdpConfigForClient,
listIdpConfigsForClient,
} from "../../../lib/devApi";
import type { IdpConfig, IdpConfigCreateRequest } from "../../../lib/devApi";
import { t } from "../../../lib/i18n";
// Proper Modal Component with Form
@@ -177,9 +177,16 @@ const CreateIdpModal = ({
};
export function ClientFederationPage() {
const { id: clientId } = useParams<{ id: string }>();
const { id: clientIdParam } = useParams<{ id: string }>();
const clientId = clientIdParam ?? "";
const [isCreateModalOpen, setCreateModalOpen] = useState(false);
const { data, isLoading, error } = useQuery({
queryKey: ["idpConfigs", clientId],
queryFn: () => listIdpConfigsForClient(clientId),
enabled: clientId.length > 0,
});
if (!clientId) {
return (
<div className="p-8 text-center text-destructive">
@@ -188,11 +195,6 @@ export function ClientFederationPage() {
);
}
const { data, isLoading, error } = useQuery({
queryKey: ["idpConfigs", clientId],
queryFn: () => listIdpConfigsForClient(clientId),
});
return (
<div className="space-y-6 p-1">
<header className="flex flex-wrap items-center justify-between gap-4">

View File

@@ -1,5 +1,5 @@
import type { TenantSummary, UserSummary } from "../../lib/adminApi";
import { type TenantNode, buildTenantFullTree } from "../../lib/tenantTree";
import { buildTenantFullTree, type TenantNode } from "../../lib/tenantTree";
import { orderHanmacFamilyChildren } from "./hanmacFamilyOrder";
import type { OrgPickerTreeNode } from "./pickerTypes";
import { filterTenantsByVisibility } from "./tenantVisibility";

View File

@@ -16,9 +16,7 @@ describe("org chart rank priority", () => {
it("orders executive and research ranks with shared priority weights", () => {
expect(getOrgRankWeight("사장")).toBeLessThan(getOrgRankWeight("부사장"));
expect(getOrgRankWeight("전무이사")).toBeLessThan(
getOrgRankWeight("상무"),
);
expect(getOrgRankWeight("전무이사")).toBeLessThan(getOrgRankWeight("상무"));
expect(getOrgRankWeight("수석연구원")).toBeLessThan(
getOrgRankWeight("책임"),
);

View File

@@ -1,6 +1,5 @@
import { describe, expect, it } from "vitest";
import {
type OrgNode,
buildOrgSelectionOptions,
buildUsersMap,
clampScale,
@@ -9,6 +8,7 @@ import {
getOrgNodeHeaderFill,
getSemanticZoomMode,
layoutForest,
type OrgNode,
} from "./OrgChartPage";
function orgNode(id: string, children: OrgNode[] = [], level = 0): OrgNode {
@@ -137,7 +137,9 @@ describe("org chart layout", () => {
],
new Set(),
);
const shortNode = shortLayout.nodes.find((item) => item.node.id === "short");
const shortNode = shortLayout.nodes.find(
(item) => item.node.id === "short",
);
const longNode = longLayout.nodes.find((item) => item.node.id === "long");
expect(shortNode?.width).toBeLessThan(320);
@@ -472,16 +474,31 @@ describe("org chart layout", () => {
"visible-parent",
);
const internalOrg = {
...tenantNode("internal-org", "ORGANIZATION", "내부 조직", "internal-org"),
...tenantNode(
"internal-org",
"ORGANIZATION",
"내부 조직",
"internal-org",
),
parentId: "visible-parent",
config: { visibility: "internal" },
};
const internalChild = {
...tenantNode("internal-child", "ORGANIZATION", "내부 하위", "internal-child"),
...tenantNode(
"internal-child",
"ORGANIZATION",
"내부 하위",
"internal-child",
),
parentId: "internal-org",
};
const privateOrg = {
...tenantNode("private-org", "ORGANIZATION", "비공개 조직", "private-org"),
...tenantNode(
"private-org",
"ORGANIZATION",
"비공개 조직",
"private-org",
),
parentId: "visible-parent",
config: { visibility: "private" },
};

View File

@@ -3,19 +3,19 @@ import type { Node as ReactFlowNode } from "@xyflow/react";
import * as React from "react";
import { useLocation, useParams } from "react-router-dom";
import {
type TenantSummary,
type UserSummary,
fetchAllTenants,
fetchPublicOrgChart,
fetchUsers,
type TenantSummary,
type UserSummary,
} from "../../../lib/adminApi";
import { type TenantNode, buildTenantFullTree } from "../../../lib/tenantTree";
import { buildTenantFullTree, type TenantNode } from "../../../lib/tenantTree";
import {
orderHanmacFamilyChildren,
orderHanmacFamilyTenants,
} from "../hanmacFamilyOrder";
import { filterTenantsByVisibility, getOrgUnitType } from "../tenantVisibility";
import { getOrgRankWeight } from "../rankPriority";
import { filterTenantsByVisibility, getOrgUnitType } from "../tenantVisibility";
import { getOrgChartUserDisplayName, getUserOrgProfile } from "../userDisplay";
export type OrgNode = {
@@ -160,7 +160,7 @@ function getComplementaryColor(hexColor: string) {
function getDisplayTextWidthUnit(value: string) {
return Array.from(value).reduce((sum, char) => {
if (char === " ") return sum + 0.4;
if (/^[\x00-\x7f]$/.test(char)) return sum + 0.55;
if (char.charCodeAt(0) <= 0x7f) return sum + 0.55;
return sum + 1;
}, 0);
}
@@ -206,7 +206,7 @@ export function getMemberGridMetrics(
? NODE_WIDTH
: Math.max(
NODE_WIDTH,
NODE_PADDING_Y * 2 +
NODE_PADDING_Y * 2 +
columnCount * memberColumnWidth +
(columnCount - 1) * MEMBER_COLUMN_GAP,
);

View File

@@ -1,10 +1,10 @@
import * as React from "react";
import { useLocation } from "react-router-dom";
import {
buildOrgPickerEmbedSrc,
type OrgPickerEmbedOptions,
type OrgPickerMode,
type OrgPickerSelectableType,
buildOrgPickerEmbedSrc,
parseOrgPickerEmbedOptions,
} from "../pickerTypes";

View File

@@ -6,14 +6,14 @@ import { Button } from "../../../components/ui/button";
import { fetchAllTenants, fetchUsers } from "../../../lib/adminApi";
import { buildOrgPickerTree, flattenDescendants } from "../pickerTree";
import {
buildOrgPickerEmbedSrc,
nodeKey,
type OrgPickerEmbedOptions,
type OrgPickerMode,
type OrgPickerResult,
type OrgPickerSelectableType,
type OrgPickerSelection,
type OrgPickerTreeNode,
buildOrgPickerEmbedSrc,
nodeKey,
parseOrgPickerEmbedOptions,
parseOrgPickerMode,
parseOrgPickerSelectableType,

View File

@@ -1,9 +1,6 @@
import { describe, expect, it } from "vitest";
import type { UserSummary } from "../../lib/adminApi";
import {
getOrgChartUserDisplayName,
getUserOrgProfile,
} from "./userDisplay";
import { getOrgChartUserDisplayName, getUserOrgProfile } from "./userDisplay";
function user(overrides: Partial<UserSummary>): UserSummary {
return {