Standardize API response envelopes: use items for collection responses and update tests
This commit is contained in:
25
frontend/src/api/__tests__/jails.test.ts
Normal file
25
frontend/src/api/__tests__/jails.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { Mock } from "vitest";
|
||||
import { ENDPOINTS } from "../endpoints";
|
||||
import { fetchIgnoreList } from "../jails";
|
||||
import { get } from "../client";
|
||||
|
||||
vi.mock("../client", () => ({
|
||||
get: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockedGet = get as Mock;
|
||||
|
||||
describe("fetchIgnoreList", () => {
|
||||
beforeEach(() => {
|
||||
mockedGet.mockReset();
|
||||
mockedGet.mockResolvedValue({ items: ["127.0.0.1"], total: 1 });
|
||||
});
|
||||
|
||||
it("requests the jail ignore list endpoint and returns the wrapped response", async () => {
|
||||
const response = await fetchIgnoreList("sshd");
|
||||
|
||||
expect(get).toHaveBeenCalledWith(ENDPOINTS.jailIgnoreIp("sshd"));
|
||||
expect(response).toEqual({ items: ["127.0.0.1"], total: 1 });
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import { del, get, post } from "./client";
|
||||
import { ENDPOINTS } from "./endpoints";
|
||||
import type {
|
||||
ActiveBanListResponse,
|
||||
IgnoreListResponse,
|
||||
IpLookupResponse,
|
||||
JailBannedIpsResponse,
|
||||
JailCommandResponse,
|
||||
@@ -112,11 +113,11 @@ export async function reloadAllJails(): Promise<JailCommandResponse> {
|
||||
* Return the ignore list for a jail.
|
||||
*
|
||||
* @param name - Jail name.
|
||||
* @returns Array of IP addresses / CIDR networks on the ignore list.
|
||||
* @returns The {@link IgnoreListResponse} wrapper containing the ignore list.
|
||||
* @throws {ApiError} On non-2xx responses.
|
||||
*/
|
||||
export async function fetchIgnoreList(name: string): Promise<string[]> {
|
||||
return get<string[]>(ENDPOINTS.jailIgnoreIp(name));
|
||||
export async function fetchIgnoreList(name: string): Promise<IgnoreListResponse> {
|
||||
return get<IgnoreListResponse>(ENDPOINTS.jailIgnoreIp(name));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -107,7 +107,7 @@ vi.mock("../../api/config", () => ({
|
||||
createActionFile: vi.fn(),
|
||||
previewLog: vi.fn(),
|
||||
testRegex: vi.fn(),
|
||||
fetchInactiveJails: vi.fn().mockResolvedValue({ jails: [], total: 0 }),
|
||||
fetchInactiveJails: vi.fn().mockResolvedValue({ items: [], total: 0 }),
|
||||
activateJail: vi.fn(),
|
||||
deactivateJail: vi.fn(),
|
||||
fetchParsedFilter: vi.fn(),
|
||||
@@ -139,7 +139,7 @@ vi.mock("../../api/config", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../../api/jails", () => ({
|
||||
fetchJails: vi.fn().mockResolvedValue({ jails: [], total: 0 }),
|
||||
fetchJails: vi.fn().mockResolvedValue({ items: [], total: 0 }),
|
||||
}));
|
||||
|
||||
/** Minimal jail fixture used across tests. */
|
||||
@@ -185,7 +185,7 @@ async function openSshdAccordion(user: ReturnType<typeof userEvent.setup>) {
|
||||
describe("ConfigPage — Add Log Path", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockFetchJailConfigs.mockResolvedValue({ jails: [MOCK_JAIL], total: 1 });
|
||||
mockFetchJailConfigs.mockResolvedValue({ items: [MOCK_JAIL], total: 1 });
|
||||
mockAddLogPath.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ const mockFetchJails = vi.mocked(fetchJails);
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const mockJailsResponse: JailListResponse = {
|
||||
jails: [
|
||||
items: [
|
||||
{
|
||||
name: "sshd",
|
||||
enabled: true,
|
||||
|
||||
@@ -12,7 +12,7 @@ vi.mock("../../../hooks/useAutoSave");
|
||||
vi.mock("../../../hooks/useJailConfigs");
|
||||
vi.mock("../../../hooks/useConfigActiveStatus");
|
||||
vi.mock("../../../api/config", () => ({
|
||||
fetchInactiveJails: vi.fn().mockResolvedValue({ jails: [] }),
|
||||
fetchInactiveJails: vi.fn().mockResolvedValue({ items: [] }),
|
||||
deactivateJail: vi.fn(),
|
||||
deleteJailLocalOverride: vi.fn(),
|
||||
addLogPath: vi.fn(),
|
||||
|
||||
@@ -11,7 +11,7 @@ describe("useJailBannedIps", () => {
|
||||
const unbanMock = vi.mocked(api.unbanIp);
|
||||
|
||||
fetchMock.mockResolvedValue({ items: [{ ip: "1.2.3.4", jail: "sshd", banned_at: "2025-01-01T10:00:00+00:00", expires_at: "2025-01-01T10:10:00+00:00", ban_count: 1, country: "US" }], total: 1, page: 1, page_size: 25 });
|
||||
unbanMock.mockResolvedValue({ message: "ok", jail: "sshd" });
|
||||
unbanMock.mockResolvedValue({ message: "ok", jail: "sshd", success: true });
|
||||
|
||||
const { result } = renderHook(() => useJailBannedIps("sshd"));
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -12,7 +12,7 @@ describe("useJailConfigs", () => {
|
||||
|
||||
it("calls fetchJailConfigs only once on mount", async () => {
|
||||
vi.mocked(configApi.fetchJailConfigs).mockResolvedValue({
|
||||
jails: [
|
||||
items: [
|
||||
{
|
||||
name: "sshd",
|
||||
ban_time: 600,
|
||||
@@ -49,7 +49,7 @@ describe("useJailConfigs", () => {
|
||||
|
||||
it("does not trigger infinite refetch with stable onSuccess", async () => {
|
||||
vi.mocked(configApi.fetchJailConfigs).mockResolvedValue({
|
||||
jails: [],
|
||||
items: [],
|
||||
total: 0,
|
||||
});
|
||||
|
||||
|
||||
@@ -10,12 +10,14 @@ vi.mock("../../api/jails");
|
||||
|
||||
const mockJail: Jail = {
|
||||
name: "sshd",
|
||||
enabled: true,
|
||||
running: true,
|
||||
idle: false,
|
||||
backend: "pyinotify",
|
||||
log_paths: ["/var/log/auth.log"],
|
||||
fail_regex: ["^\\[.*\\]\\s.*Failed password"],
|
||||
fail_regex: ["^\[.*\]\s.*Failed password"],
|
||||
ignore_regex: [],
|
||||
ignore_ips: [],
|
||||
date_pattern: "%b %d %H:%M:%S",
|
||||
log_encoding: "UTF-8",
|
||||
actions: [],
|
||||
@@ -87,13 +89,13 @@ describe("useJailCommands — write operations", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(jailsApi.startJail).mockResolvedValue({ message: "ok", jail: "sshd" });
|
||||
vi.mocked(jailsApi.stopJail).mockResolvedValue({ message: "ok", jail: "sshd" });
|
||||
vi.mocked(jailsApi.reloadJail).mockResolvedValue({ message: "ok", jail: "sshd" });
|
||||
vi.mocked(jailsApi.setJailIdle).mockResolvedValue({ message: "ok", jail: "sshd" });
|
||||
vi.mocked(jailsApi.addIgnoreIp).mockResolvedValue({ message: "ok", jail: "sshd" });
|
||||
vi.mocked(jailsApi.delIgnoreIp).mockResolvedValue({ message: "ok", jail: "sshd" });
|
||||
vi.mocked(jailsApi.toggleIgnoreSelf).mockResolvedValue({ message: "ok", jail: "sshd" });
|
||||
vi.mocked(jailsApi.startJail).mockResolvedValue({ message: "ok", jail: "sshd", success: true });
|
||||
vi.mocked(jailsApi.stopJail).mockResolvedValue({ message: "ok", jail: "sshd", success: true });
|
||||
vi.mocked(jailsApi.reloadJail).mockResolvedValue({ message: "ok", jail: "sshd", success: true });
|
||||
vi.mocked(jailsApi.setJailIdle).mockResolvedValue({ message: "ok", jail: "sshd", success: true });
|
||||
vi.mocked(jailsApi.addIgnoreIp).mockResolvedValue({ message: "ok", jail: "sshd", success: true });
|
||||
vi.mocked(jailsApi.delIgnoreIp).mockResolvedValue({ message: "ok", jail: "sshd", success: true });
|
||||
vi.mocked(jailsApi.toggleIgnoreSelf).mockResolvedValue({ message: "ok", jail: "sshd", success: true });
|
||||
});
|
||||
|
||||
it("calls start() API and invokes onSuccess", async () => {
|
||||
|
||||
@@ -69,11 +69,11 @@ vi.mock("../../hooks/useJailBannedIps", () => ({
|
||||
|
||||
// Mock API functions used by JailInfoSection control buttons to avoid side effects.
|
||||
vi.mock("../../api/jails", () => ({
|
||||
startJail: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd" }),
|
||||
stopJail: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd" }),
|
||||
reloadJail: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd" }),
|
||||
setJailIdle: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd" }),
|
||||
toggleIgnoreSelf: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd" }),
|
||||
startJail: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd", success: true }),
|
||||
stopJail: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd", success: true }),
|
||||
reloadJail: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd", success: true }),
|
||||
setJailIdle: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd", success: true }),
|
||||
toggleIgnoreSelf: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd", success: true }),
|
||||
}));
|
||||
|
||||
// Stub BannedIpsSection to prevent its own fetchJailBannedIps calls.
|
||||
@@ -92,12 +92,14 @@ import { useJailCommands } from "../../hooks/useJailCommands";
|
||||
function makeJail(): Jail {
|
||||
return {
|
||||
name: "sshd",
|
||||
enabled: true,
|
||||
running: true,
|
||||
idle: false,
|
||||
backend: "systemd",
|
||||
log_paths: ["/var/log/auth.log"],
|
||||
fail_regex: ["^Failed .+ from <HOST>"],
|
||||
ignore_regex: [],
|
||||
ignore_ips: [],
|
||||
date_pattern: "",
|
||||
log_encoding: "UTF-8",
|
||||
actions: ["iptables-multiport"],
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* `backend/app/models/ban.py` — dashboard dashboard sections.
|
||||
*/
|
||||
|
||||
import type { PaginatedListResponse } from "./response";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Time-range selector
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -57,16 +59,7 @@ export interface DashboardBanItem {
|
||||
*
|
||||
* Mirrors `DashboardBanListResponse` from `backend/app/models/ban.py`.
|
||||
*/
|
||||
export interface DashboardBanListResponse {
|
||||
/** Ban items for the current page. */
|
||||
items: DashboardBanItem[];
|
||||
/** Total number of bans in the selected time window. */
|
||||
total: number;
|
||||
/** Current 1-based page number. */
|
||||
page: number;
|
||||
/** Maximum items per page. */
|
||||
page_size: number;
|
||||
}
|
||||
export interface DashboardBanListResponse extends PaginatedListResponse<DashboardBanItem> {}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ban trend
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* TypeScript interfaces for the configuration and server settings API.
|
||||
*/
|
||||
|
||||
import type { CollectionResponse } from "./response";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ban-time escalation
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -66,10 +68,7 @@ export interface JailConfigResponse {
|
||||
jail: JailConfig;
|
||||
}
|
||||
|
||||
export interface JailConfigListResponse {
|
||||
items: JailConfig[];
|
||||
total: number;
|
||||
}
|
||||
export interface JailConfigListResponse extends CollectionResponse<JailConfig> {}
|
||||
|
||||
export interface JailConfigUpdate {
|
||||
ban_time?: number | null;
|
||||
@@ -535,10 +534,7 @@ export interface InactiveJail {
|
||||
has_local_override: boolean;
|
||||
}
|
||||
|
||||
export interface InactiveJailListResponse {
|
||||
items: InactiveJail[];
|
||||
total: number;
|
||||
}
|
||||
export interface InactiveJailListResponse extends CollectionResponse<InactiveJail> {}
|
||||
|
||||
/**
|
||||
* Optional override values when activating an inactive jail.
|
||||
|
||||
@@ -7,7 +7,16 @@
|
||||
* - `backend/app/models/geo.py` (GeoDetail / IpLookupResponse)
|
||||
*/
|
||||
|
||||
import type { BantimeEscalation, BackendType, LogEncoding } from "./config";
|
||||
import type {
|
||||
BantimeEscalation,
|
||||
BackendType,
|
||||
LogEncoding,
|
||||
} from "./config";
|
||||
import type {
|
||||
CollectionResponse,
|
||||
PaginatedListResponse,
|
||||
CommandResponse,
|
||||
} from "./response";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Jail statistics
|
||||
@@ -64,12 +73,14 @@ export interface JailSummary {
|
||||
*
|
||||
* Mirrors `JailListResponse` from `backend/app/models/jail.py`.
|
||||
*/
|
||||
export interface JailListResponse {
|
||||
/** All known jails. */
|
||||
items: JailSummary[];
|
||||
/** Total number of jails. */
|
||||
total: number;
|
||||
}
|
||||
export interface JailListResponse extends CollectionResponse<JailSummary> {}
|
||||
|
||||
/**
|
||||
* Response from `GET /api/jails/{name}/ignoreip`.
|
||||
*
|
||||
* Mirrors `IgnoreListResponse` from `backend/app/models/jail.py`.
|
||||
*/
|
||||
export interface IgnoreListResponse extends CollectionResponse<string> {}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Jail detail
|
||||
@@ -83,6 +94,8 @@ export interface JailListResponse {
|
||||
export interface Jail {
|
||||
/** Machine-readable jail name. */
|
||||
name: string;
|
||||
/** Whether the jail is enabled in the configuration. */
|
||||
enabled: boolean;
|
||||
/** Whether the jail is running. */
|
||||
running: boolean;
|
||||
/** Whether the jail is in idle mode. */
|
||||
@@ -95,8 +108,10 @@ export interface Jail {
|
||||
fail_regex: string[];
|
||||
/** Ignore-regex patterns used to whitelist log lines. */
|
||||
ignore_regex: string[];
|
||||
/** Date-pattern used for timestamp parsing, or empty string. */
|
||||
date_pattern: string;
|
||||
/** IP addresses and networks explicitly ignored by the jail. */
|
||||
ignore_ips: string[];
|
||||
/** Date-pattern used for timestamp parsing, or `null` when unset. */
|
||||
date_pattern: string | null;
|
||||
/** Log file encoding (e.g. `"UTF-8"`). */
|
||||
log_encoding: LogEncoding;
|
||||
/** Action names attached to this jail. */
|
||||
@@ -136,9 +151,7 @@ export interface JailDetailResponse {
|
||||
*
|
||||
* Mirrors `JailCommandResponse` from `backend/app/models/jail.py`.
|
||||
*/
|
||||
export interface JailCommandResponse {
|
||||
/** Human-readable result message. */
|
||||
message: string;
|
||||
export interface JailCommandResponse extends CommandResponse {
|
||||
/** Target jail name, or `"*"` for operations on all jails. */
|
||||
jail: string;
|
||||
}
|
||||
@@ -172,12 +185,7 @@ export interface ActiveBan {
|
||||
*
|
||||
* Mirrors `ActiveBanListResponse` from `backend/app/models/ban.py`.
|
||||
*/
|
||||
export interface ActiveBanListResponse {
|
||||
/** List of all currently active bans. */
|
||||
items: ActiveBan[];
|
||||
/** Total number of active bans. */
|
||||
total: number;
|
||||
}
|
||||
export interface ActiveBanListResponse extends CollectionResponse<ActiveBan> {}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Geo / IP lookup
|
||||
@@ -207,16 +215,7 @@ export interface UnbanAllResponse {
|
||||
*
|
||||
* Mirrors `JailBannedIpsResponse` from `backend/app/models/ban.py`.
|
||||
*/
|
||||
export interface JailBannedIpsResponse {
|
||||
/** Active ban entries for the current page. */
|
||||
items: ActiveBan[];
|
||||
/** Total matching entries (after applying any search filter). */
|
||||
total: number;
|
||||
/** Current page number (1-based). */
|
||||
page: number;
|
||||
/** Number of items per page. */
|
||||
page_size: number;
|
||||
}
|
||||
export interface JailBannedIpsResponse extends PaginatedListResponse<ActiveBan> {}
|
||||
|
||||
export interface GeoDetail {
|
||||
/** ISO 3166-1 alpha-2 country code (e.g. `"DE"`), or `null`. */
|
||||
|
||||
27
frontend/src/types/response.ts
Normal file
27
frontend/src/types/response.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Standardized API response envelope types.
|
||||
*
|
||||
* These wrappers mirror the backend response envelope contract defined in
|
||||
* `backend/app/models/response.py`.
|
||||
*/
|
||||
|
||||
export interface CollectionResponse<T> {
|
||||
/** Data items in the collection. */
|
||||
items: T[];
|
||||
/** Total number of items in the collection. */
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface PaginatedListResponse<T> extends CollectionResponse<T> {
|
||||
/** Current page number (1-based). */
|
||||
page: number;
|
||||
/** Number of items per page. */
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
export interface CommandResponse {
|
||||
/** Human-readable result message. */
|
||||
message: string;
|
||||
/** Whether the command succeeded. */
|
||||
success: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user