mirror of
https://github.com/dstotijn/hetty
synced 2024-11-22 11:43:09 +00:00
Add Sender module
This commit is contained in:
parent
afa211d0ec
commit
efc20564c1
58 changed files with 9131 additions and 282 deletions
|
@ -1,6 +1,18 @@
|
||||||
{
|
{
|
||||||
"extends": ["next/core-web-vitals", "prettier"],
|
"extends": ["next/core-web-vitals", "prettier"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@next/next/no-css-tags": "off"
|
"@next/next/no-css-tags": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
"unused-imports/no-unused-imports": "error",
|
||||||
|
"unused-imports/no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"vars": "all",
|
||||||
|
"varsIgnorePattern": "^_",
|
||||||
|
"args": "after-used",
|
||||||
|
"argsIgnorePattern": "^_"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"plugins": ["unused-imports", "prettier"]
|
||||||
}
|
}
|
||||||
|
|
9
admin/gqlcodegen.yml
Normal file
9
admin/gqlcodegen.yml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
overwrite: true
|
||||||
|
schema: "../pkg/api/schema.graphql"
|
||||||
|
documents: "src/**/*.graphql"
|
||||||
|
generates:
|
||||||
|
src/generated/graphql.tsx:
|
||||||
|
plugins:
|
||||||
|
- "typescript"
|
||||||
|
- "typescript-operations"
|
||||||
|
- "typescript-react-apollo"
|
|
@ -7,7 +7,8 @@
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"export": "next build && next export -o dist"
|
"export": "next build && next export -o dist",
|
||||||
|
"generate": "graphql-codegen --config gqlcodegen.yml"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.2.0",
|
"@apollo/client": "^3.2.0",
|
||||||
|
@ -18,6 +19,7 @@
|
||||||
"@mui/icons-material": "^5.3.1",
|
"@mui/icons-material": "^5.3.1",
|
||||||
"@mui/lab": "^5.0.0-alpha.66",
|
"@mui/lab": "^5.0.0-alpha.66",
|
||||||
"@mui/material": "^5.3.1",
|
"@mui/material": "^5.3.1",
|
||||||
|
"allotment": "^1.9.0",
|
||||||
"deepmerge": "^4.2.2",
|
"deepmerge": "^4.2.2",
|
||||||
"graphql": "^16.2.0",
|
"graphql": "^16.2.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
@ -29,13 +31,21 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.0.0",
|
"@babel/core": "^7.0.0",
|
||||||
|
"@graphql-codegen/cli": "2.6.1",
|
||||||
|
"@graphql-codegen/introspection": "2.1.1",
|
||||||
|
"@graphql-codegen/typescript": "2.4.3",
|
||||||
|
"@graphql-codegen/typescript-operations": "2.3.0",
|
||||||
|
"@graphql-codegen/typescript-react-apollo": "3.2.6",
|
||||||
"@types/lodash": "^4.14.178",
|
"@types/lodash": "^4.14.178",
|
||||||
"@types/node": "^17.0.12",
|
"@types/node": "^17.0.12",
|
||||||
"@types/react": "^17.0.38",
|
"@types/react": "^17.0.38",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.12.0",
|
||||||
|
"@typescript-eslint/parser": "^5.12.0",
|
||||||
"eslint": "^8.7.0",
|
"eslint": "^8.7.0",
|
||||||
"eslint-config-next": "12.0.8",
|
"eslint-config-next": "12.0.8",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
"prettier": "^2.1.2",
|
"prettier": "^2.1.2",
|
||||||
"typescript": "^4.0.3",
|
"typescript": "^4.0.3",
|
||||||
"webpack": "^5.67.0"
|
"webpack": "^5.67.0"
|
||||||
|
|
|
@ -151,7 +151,7 @@ export function Layout({ title, page, children }: Props): JSX.Element {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex" }}>
|
<Box sx={{ display: "flex", height: "100%" }}>
|
||||||
<AppBar position="fixed" open={open}>
|
<AppBar position="fixed" open={open}>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -241,8 +241,7 @@ export function Layout({ title, page, children }: Props): JSX.Element {
|
||||||
</Link>
|
</Link>
|
||||||
</List>
|
</List>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
|
<Box component="main" sx={{ flexGrow: 1, mx: 3, mt: 11 }}>
|
||||||
<DrawerHeader />
|
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import MonacoEditor from "@monaco-editor/react";
|
import MonacoEditor, { EditorProps } from "@monaco-editor/react";
|
||||||
import monaco from "monaco-editor/esm/vs/editor/editor.api";
|
|
||||||
|
|
||||||
const monacoOptions: monaco.editor.IEditorOptions = {
|
const defaultMonacoOptions: EditorProps["options"] = {
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
wordWrap: "on",
|
wordWrap: "on",
|
||||||
minimap: {
|
minimap: {
|
||||||
|
@ -12,8 +11,9 @@ const monacoOptions: monaco.editor.IEditorOptions = {
|
||||||
type language = "html" | "typescript" | "json";
|
type language = "html" | "typescript" | "json";
|
||||||
|
|
||||||
function languageForContentType(contentType?: string): language | undefined {
|
function languageForContentType(contentType?: string): language | undefined {
|
||||||
switch (contentType) {
|
switch (contentType?.toLowerCase()) {
|
||||||
case "text/html":
|
case "text/html":
|
||||||
|
case "text/html; charset=utf-8":
|
||||||
return "html";
|
return "html";
|
||||||
case "application/json":
|
case "application/json":
|
||||||
case "application/json; charset=utf-8":
|
case "application/json; charset=utf-8":
|
||||||
|
@ -29,16 +29,19 @@ function languageForContentType(contentType?: string): language | undefined {
|
||||||
interface Props {
|
interface Props {
|
||||||
content: string;
|
content: string;
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
|
monacoOptions?: EditorProps["options"];
|
||||||
|
onChange?: EditorProps["onChange"];
|
||||||
}
|
}
|
||||||
|
|
||||||
function Editor({ content, contentType }: Props): JSX.Element {
|
function Editor({ content, contentType, monacoOptions, onChange }: Props): JSX.Element {
|
||||||
|
console.log(content);
|
||||||
return (
|
return (
|
||||||
<MonacoEditor
|
<MonacoEditor
|
||||||
height={"600px"}
|
|
||||||
language={languageForContentType(contentType)}
|
language={languageForContentType(contentType)}
|
||||||
theme="vs-dark"
|
theme="vs-dark"
|
||||||
options={monacoOptions}
|
options={{ ...defaultMonacoOptions, ...monacoOptions }}
|
||||||
value={content}
|
value={content}
|
||||||
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
37
admin/src/components/common/ResponseStatus.tsx
Normal file
37
admin/src/components/common/ResponseStatus.tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { Typography } from "@mui/material";
|
||||||
|
|
||||||
|
import HttpStatusIcon from "./HttpStatusIcon";
|
||||||
|
import { HttpProtocol } from "../../generated/graphql";
|
||||||
|
|
||||||
|
type ResponseStatusProps = {
|
||||||
|
proto: HttpProtocol;
|
||||||
|
statusCode: number;
|
||||||
|
statusReason: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapProto(proto: HttpProtocol): string {
|
||||||
|
switch (proto) {
|
||||||
|
case HttpProtocol.Http1:
|
||||||
|
return "HTTP/1.1";
|
||||||
|
case HttpProtocol.Http2:
|
||||||
|
return "HTTP/2.0";
|
||||||
|
default:
|
||||||
|
return proto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResponseStatus({ proto, statusCode, statusReason }: ResponseStatusProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Typography variant="h6" style={{ fontSize: "1rem", whiteSpace: "nowrap" }}>
|
||||||
|
<HttpStatusIcon status={statusCode} />{" "}
|
||||||
|
<Typography component="span" color="textSecondary">
|
||||||
|
<Typography component="span" color="textSecondary" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
|
||||||
|
{mapProto(proto)}
|
||||||
|
</Typography>
|
||||||
|
</Typography>{" "}
|
||||||
|
{statusCode} {statusReason}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResponseStatus;
|
47
admin/src/components/common/useContextMenu.tsx
Normal file
47
admin/src/components/common/useContextMenu.tsx
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { Menu } from "@mui/material";
|
||||||
|
|
||||||
|
export interface ContextMenuProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useContextMenu(): [(props: ContextMenuProps) => JSX.Element, (e: React.MouseEvent) => void, () => void] {
|
||||||
|
const [contextMenu, setContextMenu] = React.useState<{
|
||||||
|
mouseX: number;
|
||||||
|
mouseY: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const handleContextMenu = (event: React.MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setContextMenu(
|
||||||
|
contextMenu === null
|
||||||
|
? {
|
||||||
|
mouseX: event.clientX - 2,
|
||||||
|
mouseY: event.clientY - 4,
|
||||||
|
}
|
||||||
|
: // repeated contextmenu when it is already open closes it with Chrome 84 on Ubuntu
|
||||||
|
// Other native context menus might behave different.
|
||||||
|
// With this behavior we prevent contextmenu from the backdrop to re-locale existing context menus.
|
||||||
|
null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setContextMenu(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const menu = ({ children }: ContextMenuProps): JSX.Element => (
|
||||||
|
<Menu
|
||||||
|
open={contextMenu !== null}
|
||||||
|
onClose={handleClose}
|
||||||
|
anchorReference="anchorPosition"
|
||||||
|
anchorPosition={contextMenu !== null ? { top: contextMenu.mouseY, left: contextMenu.mouseX } : undefined}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
return [menu, handleContextMenu, handleClose];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useContextMenu;
|
|
@ -68,7 +68,13 @@ const DELETE_PROJECT = gql`
|
||||||
|
|
||||||
function ProjectList(): JSX.Element {
|
function ProjectList(): JSX.Element {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { loading: projLoading, error: projErr, data: projData } = useQuery<{ projects: Project[] }>(PROJECTS);
|
const {
|
||||||
|
loading: projLoading,
|
||||||
|
error: projErr,
|
||||||
|
data: projData,
|
||||||
|
} = useQuery<{ projects: Project[] }>(PROJECTS, {
|
||||||
|
fetchPolicy: "network-only",
|
||||||
|
});
|
||||||
const [openProject, { error: openProjErr, loading: openProjLoading }] = useMutation<{ openProject: Project }>(
|
const [openProject, { error: openProjErr, loading: openProjLoading }] = useMutation<{ openProject: Project }>(
|
||||||
OPEN_PROJECT,
|
OPEN_PROJECT,
|
||||||
{
|
{
|
||||||
|
@ -114,9 +120,12 @@ function ProjectList(): JSX.Element {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const [closeProject, { error: closeProjErr }] = useMutation(CLOSE_PROJECT, {
|
const [closeProject, { error: closeProjErr, client }] = useMutation(CLOSE_PROJECT, {
|
||||||
errorPolicy: "all",
|
errorPolicy: "all",
|
||||||
onError: () => {},
|
onError: () => {},
|
||||||
|
onCompleted() {
|
||||||
|
client.resetStore();
|
||||||
|
},
|
||||||
update(cache) {
|
update(cache) {
|
||||||
cache.modify({
|
cache.modify({
|
||||||
fields: {
|
fields: {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import Alert from "@mui/lab/Alert";
|
||||||
|
|
||||||
import RequestList from "./RequestList";
|
import RequestList from "./RequestList";
|
||||||
import LogDetail from "./LogDetail";
|
import LogDetail from "./LogDetail";
|
||||||
import CenteredPaper from "../CenteredPaper";
|
import CenteredPaper from "../common/CenteredPaper";
|
||||||
import { useHttpRequestLogs } from "./hooks/useHttpRequestLogs";
|
import { useHttpRequestLogs } from "./hooks/useHttpRequestLogs";
|
||||||
|
|
||||||
function LogsOverview(): JSX.Element {
|
function LogsOverview(): JSX.Element {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from "react";
|
||||||
import { Typography, Box, Divider } from "@mui/material";
|
import { Typography, Box, Divider } from "@mui/material";
|
||||||
|
|
||||||
import HttpHeadersTable from "./HttpHeadersTable";
|
import HttpHeadersTable from "./HttpHeadersTable";
|
||||||
import Editor from "./Editor";
|
import Editor from "../common/Editor";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
request: {
|
request: {
|
||||||
|
|
|
@ -9,11 +9,18 @@ import {
|
||||||
Typography,
|
Typography,
|
||||||
Box,
|
Box,
|
||||||
useTheme,
|
useTheme,
|
||||||
|
MenuItem,
|
||||||
|
Snackbar,
|
||||||
|
Alert,
|
||||||
|
Link,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
import HttpStatusIcon from "./HttpStatusCode";
|
import HttpStatusIcon from "../common/HttpStatusIcon";
|
||||||
import CenteredPaper from "../CenteredPaper";
|
import CenteredPaper from "../common/CenteredPaper";
|
||||||
import { RequestLog } from "../../lib/requestLogs";
|
import { RequestLog } from "../../lib/requestLogs";
|
||||||
|
import useContextMenu from "../common/useContextMenu";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useCreateSenderRequestFromHttpRequestLogMutation } from "../../generated/graphql";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
logs: RequestLog[];
|
logs: RequestLog[];
|
||||||
|
@ -45,7 +52,50 @@ interface RequestListTableProps {
|
||||||
function RequestListTable({ logs, selectedReqLogId, onLogClick }: RequestListTableProps): JSX.Element {
|
function RequestListTable({ logs, selectedReqLogId, onLogClick }: RequestListTableProps): JSX.Element {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const [createSenderReqFromLog] = useCreateSenderRequestFromHttpRequestLogMutation({});
|
||||||
|
|
||||||
|
const [copyToSenderId, setCopyToSenderId] = useState("");
|
||||||
|
const [Menu, handleContextMenu, handleContextMenuClose] = useContextMenu();
|
||||||
|
|
||||||
|
const handleCopyToSenderClick = () => {
|
||||||
|
createSenderReqFromLog({
|
||||||
|
variables: {
|
||||||
|
id: copyToSenderId,
|
||||||
|
},
|
||||||
|
onCompleted({ createSenderRequestFromHttpRequestLog }) {
|
||||||
|
const { id } = createSenderRequestFromHttpRequestLog;
|
||||||
|
setNewSenderReqId(id);
|
||||||
|
setCopiedReqNotifOpen(true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
handleContextMenuClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const [newSenderReqId, setNewSenderReqId] = React.useState("");
|
||||||
|
const [copiedReqNotifOpen, setCopiedReqNotifOpen] = React.useState(false);
|
||||||
|
const handleCloseCopiedNotif = (_: Event | React.SyntheticEvent, reason?: string) => {
|
||||||
|
if (reason === "clickaway") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCopiedReqNotifOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div>
|
||||||
|
<Menu>
|
||||||
|
<MenuItem onClick={handleCopyToSenderClick}>Copy request to Sender</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
<Snackbar
|
||||||
|
open={copiedReqNotifOpen}
|
||||||
|
autoHideDuration={3000}
|
||||||
|
onClose={handleCloseCopiedNotif}
|
||||||
|
anchorOrigin={{ horizontal: "center", vertical: "bottom" }}
|
||||||
|
>
|
||||||
|
<Alert onClose={handleCloseCopiedNotif} severity="info">
|
||||||
|
Request was copied. <Link href={`/sender?id=${newSenderReqId}`}>Edit in Sender.</Link>
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
|
||||||
<TableContainer
|
<TableContainer
|
||||||
component={Paper}
|
component={Paper}
|
||||||
style={{
|
style={{
|
||||||
|
@ -85,6 +135,10 @@ function RequestListTable({ logs, selectedReqLogId, onLogClick }: RequestListTab
|
||||||
}}
|
}}
|
||||||
hover
|
hover
|
||||||
onClick={() => onLogClick(id)}
|
onClick={() => onLogClick(id)}
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
setCopyToSenderId(id);
|
||||||
|
handleContextMenu(e);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<TableCell style={{ ...cellStyle, width: "100px" }}>
|
<TableCell style={{ ...cellStyle, width: "100px" }}>
|
||||||
<code>{method}</code>
|
<code>{method}</code>
|
||||||
|
@ -109,5 +163,6 @@ function RequestListTable({ logs, selectedReqLogId, onLogClick }: RequestListTab
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Typography, Box, Divider } from "@mui/material";
|
import { Typography, Box, Divider } from "@mui/material";
|
||||||
|
|
||||||
import HttpStatusIcon from "./HttpStatusCode";
|
import HttpStatusIcon from "../common/HttpStatusIcon";
|
||||||
import Editor from "./Editor";
|
import Editor from "../common/Editor";
|
||||||
import HttpHeadersTable from "./HttpHeadersTable";
|
import HttpHeadersTable from "./HttpHeadersTable";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
13
admin/src/components/reqlog/hooks/useCreateSenderRequest.ts
Normal file
13
admin/src/components/reqlog/hooks/useCreateSenderRequest.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { gql, useMutation } from "@apollo/client";
|
||||||
|
|
||||||
|
const CREATE_SENDER_REQUEST = gql`
|
||||||
|
mutation CreateSenderRequest($request: SenderRequestInput!) {
|
||||||
|
createSenderRequest(request: $request) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function useCreateSenderRequest() {
|
||||||
|
return useMutation(CREATE_SENDER_REQUEST);
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { gql, useApolloClient, useMutation, useQuery } from "@apollo/client";
|
import { gql, useApolloClient, useMutation } from "@apollo/client";
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Chip,
|
Chip,
|
||||||
|
|
398
admin/src/components/sender/EditRequest.tsx
Normal file
398
admin/src/components/sender/EditRequest.tsx
Normal file
|
@ -0,0 +1,398 @@
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Box,
|
||||||
|
BoxProps,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { ComponentType, FormEventHandler, useEffect, useRef, useState } from "react";
|
||||||
|
import { AllotmentProps, PaneProps } from "allotment/dist/types/src/allotment";
|
||||||
|
|
||||||
|
import { KeyValuePair, sortKeyValuePairs } from "./KeyValuePair";
|
||||||
|
import {
|
||||||
|
GetSenderRequestQuery,
|
||||||
|
HttpProtocol,
|
||||||
|
useCreateOrUpdateSenderRequestMutation,
|
||||||
|
useGetSenderRequestQuery,
|
||||||
|
useSendRequestMutation,
|
||||||
|
} from "../../generated/graphql";
|
||||||
|
import EditRequestTabs from "./EditRequestTabs";
|
||||||
|
import Response from "./Response";
|
||||||
|
|
||||||
|
import "allotment/dist/style.css";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
enum HttpMethod {
|
||||||
|
Get = "GET",
|
||||||
|
Post = "POST",
|
||||||
|
Put = "PUT",
|
||||||
|
Patch = "PATCH",
|
||||||
|
Delete = "DELETE",
|
||||||
|
Head = "HEAD",
|
||||||
|
Options = "OPTIONS",
|
||||||
|
Connect = "CONNECT",
|
||||||
|
Trace = "TRACE",
|
||||||
|
}
|
||||||
|
|
||||||
|
enum HttpProto {
|
||||||
|
Http1 = "HTTP/1.1",
|
||||||
|
Http2 = "HTTP/2.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
const httpProtoMap = new Map([
|
||||||
|
[HttpProto.Http1, HttpProtocol.Http1],
|
||||||
|
[HttpProto.Http2, HttpProtocol.Http2],
|
||||||
|
]);
|
||||||
|
|
||||||
|
function updateKeyPairItem(key: string, value: string, idx: number, items: any[]): any[] {
|
||||||
|
const updated = [...items];
|
||||||
|
updated[idx] = { key, value };
|
||||||
|
|
||||||
|
// Append an empty key-value pair if the last item in the array isn't blank
|
||||||
|
// anymore.
|
||||||
|
if (items.length - 1 === idx && items[idx].key === "" && items[idx].value === "") {
|
||||||
|
updated.push({ key: "", value: "" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateURLQueryParams(url: string, queryParams: KeyValuePair[]) {
|
||||||
|
// Note: We don't use the `URL` interface, because we're potentially dealing
|
||||||
|
// with malformed/incorrect URLs, which would yield TypeErrors when constructed
|
||||||
|
// via `URL`.
|
||||||
|
let newURL = url;
|
||||||
|
|
||||||
|
const questionMarkIndex = url.indexOf("?");
|
||||||
|
if (questionMarkIndex !== -1) {
|
||||||
|
newURL = newURL.slice(0, questionMarkIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
for (const { key, value } of queryParams.filter(({ key }) => key !== "")) {
|
||||||
|
searchParams.append(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawQueryParams = decodeURI(searchParams.toString());
|
||||||
|
|
||||||
|
if (rawQueryParams == "") {
|
||||||
|
return newURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newURL + "?" + rawQueryParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryParamsFromURL(url: string): KeyValuePair[] {
|
||||||
|
const questionMarkIndex = url.indexOf("?");
|
||||||
|
if (questionMarkIndex === -1) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParams: KeyValuePair[] = [];
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams(url.slice(questionMarkIndex + 1));
|
||||||
|
for (let [key, value] of searchParams) {
|
||||||
|
queryParams.push({ key, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditRequest(): JSX.Element {
|
||||||
|
const router = useRouter();
|
||||||
|
const reqId = router.query.id as string | undefined;
|
||||||
|
|
||||||
|
const [method, setMethod] = useState(HttpMethod.Get);
|
||||||
|
const [url, setURL] = useState("");
|
||||||
|
const [proto, setProto] = useState(HttpProto.Http2);
|
||||||
|
const [queryParams, setQueryParams] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
|
||||||
|
const [headers, setHeaders] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
|
||||||
|
const [body, setBody] = useState("");
|
||||||
|
|
||||||
|
const handleQueryParamChange = (key: string, value: string, idx: number) => {
|
||||||
|
setQueryParams((prev) => {
|
||||||
|
const updated = updateKeyPairItem(key, value, idx, prev);
|
||||||
|
setURL((prev) => updateURLQueryParams(prev, updated));
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleQueryParamDelete = (idx: number) => {
|
||||||
|
setQueryParams((prev) => {
|
||||||
|
const updated = prev.slice(0, idx).concat(prev.slice(idx + 1, prev.length));
|
||||||
|
setURL((prev) => updateURLQueryParams(prev, updated));
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHeaderChange = (key: string, value: string, idx: number) => {
|
||||||
|
setHeaders((prev) => updateKeyPairItem(key, value, idx, prev));
|
||||||
|
};
|
||||||
|
const handleHeaderDelete = (idx: number) => {
|
||||||
|
setHeaders((prev) => prev.slice(0, idx).concat(prev.slice(idx + 1, prev.length)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleURLChange = (url: string) => {
|
||||||
|
setURL(url);
|
||||||
|
|
||||||
|
const questionMarkIndex = url.indexOf("?");
|
||||||
|
if (questionMarkIndex === -1) {
|
||||||
|
setQueryParams([{ key: "", value: "" }]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newQueryParams = queryParamsFromURL(url);
|
||||||
|
// Push empty row.
|
||||||
|
newQueryParams.push({ key: "", value: "" });
|
||||||
|
setQueryParams(newQueryParams);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [response, setResponse] = useState<NonNullable<GetSenderRequestQuery["senderRequest"]>["response"]>(null);
|
||||||
|
const getReqResult = useGetSenderRequestQuery({
|
||||||
|
variables: { id: reqId as string },
|
||||||
|
skip: reqId === undefined,
|
||||||
|
onCompleted: ({ senderRequest }) => {
|
||||||
|
if (!senderRequest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setURL(senderRequest.url);
|
||||||
|
setMethod(senderRequest.method);
|
||||||
|
setBody(senderRequest.body || "");
|
||||||
|
|
||||||
|
const newQueryParams = queryParamsFromURL(senderRequest.url);
|
||||||
|
// Push empty row.
|
||||||
|
newQueryParams.push({ key: "", value: "" });
|
||||||
|
setQueryParams(newQueryParams);
|
||||||
|
|
||||||
|
const newHeaders = sortKeyValuePairs(senderRequest.headers || []);
|
||||||
|
setHeaders([...newHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]);
|
||||||
|
console.log(senderRequest.response);
|
||||||
|
setResponse(senderRequest.response);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [createOrUpdateRequest, createResult] = useCreateOrUpdateSenderRequestMutation();
|
||||||
|
const [sendRequest, sendResult] = useSendRequestMutation();
|
||||||
|
|
||||||
|
const createOrUpdateRequestAndSend = () => {
|
||||||
|
const senderReq = getReqResult?.data?.senderRequest;
|
||||||
|
createOrUpdateRequest({
|
||||||
|
variables: {
|
||||||
|
request: {
|
||||||
|
// Update existing sender request if it was cloned from a request log
|
||||||
|
// and it doesn't have a response body yet (e.g. not sent yet).
|
||||||
|
...(senderReq && senderReq.sourceRequestLogID && !senderReq.response && { id: senderReq.id }),
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
proto: httpProtoMap.get(proto),
|
||||||
|
headers: headers.filter((kv) => kv.key !== ""),
|
||||||
|
body: body || undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: ({ createOrUpdateSenderRequest }) => {
|
||||||
|
const { id } = createOrUpdateSenderRequest;
|
||||||
|
sendRequestAndPushRoute(id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendRequestAndPushRoute = (id: string) => {
|
||||||
|
sendRequest({
|
||||||
|
errorPolicy: "all",
|
||||||
|
onCompleted: () => {
|
||||||
|
router.push(`/sender?id=${id}`);
|
||||||
|
},
|
||||||
|
variables: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormSubmit: FormEventHandler = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
createOrUpdateRequestAndSend();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMountedRef = useRef(false);
|
||||||
|
const [Allotment, setAllotment] = useState<
|
||||||
|
(ComponentType<AllotmentProps> & { Pane: ComponentType<PaneProps> }) | null
|
||||||
|
>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
isMountedRef.current = true;
|
||||||
|
import("allotment")
|
||||||
|
.then((mod) => {
|
||||||
|
if (!isMountedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAllotment(mod.Allotment);
|
||||||
|
})
|
||||||
|
.catch((err) => console.error(err, `could not import allotment ${err.message}`));
|
||||||
|
return () => {
|
||||||
|
isMountedRef.current = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
if (!Allotment) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box display="flex" flexDirection="column" height="100%" gap={2}>
|
||||||
|
<Box component="form" autoComplete="off" onSubmit={handleFormSubmit}>
|
||||||
|
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||||
|
<UrlBar
|
||||||
|
method={method}
|
||||||
|
onMethodChange={setMethod}
|
||||||
|
url={url.toString()}
|
||||||
|
onUrlChange={handleURLChange}
|
||||||
|
proto={proto}
|
||||||
|
onProtoChange={setProto}
|
||||||
|
sx={{ flex: "1 auto" }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
disableElevation
|
||||||
|
sx={{ width: "8rem" }}
|
||||||
|
type="submit"
|
||||||
|
disabled={createResult.loading || sendResult.loading}
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
{createResult.error && (
|
||||||
|
<Alert severity="error" sx={{ mt: 1 }}>
|
||||||
|
{createResult.error.message}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{sendResult.error && (
|
||||||
|
<Alert severity="error" sx={{ mt: 1 }}>
|
||||||
|
{sendResult.error.message}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box flex="1 auto" overflow="hidden">
|
||||||
|
<Allotment>
|
||||||
|
<Box pr={2} pb={2} height="100%" overflow="hidden">
|
||||||
|
<Box height="100%" position="relative">
|
||||||
|
<Typography variant="overline" color="textSecondary" sx={{ position: "absolute", right: 0, mt: 1.2 }}>
|
||||||
|
Request
|
||||||
|
</Typography>
|
||||||
|
<EditRequestTabs
|
||||||
|
queryParams={queryParams}
|
||||||
|
headers={headers}
|
||||||
|
body={body}
|
||||||
|
onQueryParamChange={handleQueryParamChange}
|
||||||
|
onQueryParamDelete={handleQueryParamDelete}
|
||||||
|
onHeaderChange={handleHeaderChange}
|
||||||
|
onHeaderDelete={handleHeaderDelete}
|
||||||
|
onBodyChange={setBody}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box pb={2} pl={2} height="100%" overflow="hidden">
|
||||||
|
<Box height="100%" position="relative">
|
||||||
|
<Response response={response} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Allotment>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UrlBarProps extends BoxProps {
|
||||||
|
method: HttpMethod;
|
||||||
|
onMethodChange: (method: HttpMethod) => void;
|
||||||
|
url: string;
|
||||||
|
onUrlChange: (url: string) => void;
|
||||||
|
proto: HttpProto;
|
||||||
|
onProtoChange: (proto: HttpProto) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function UrlBar(props: UrlBarProps) {
|
||||||
|
const { method, onMethodChange, url, onUrlChange, proto, onProtoChange, ...other } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box {...other} sx={{ ...other.sx, display: "flex" }}>
|
||||||
|
<FormControl>
|
||||||
|
<InputLabel id="req-method-label">Method</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="req-method-label"
|
||||||
|
id="req-method"
|
||||||
|
value={method}
|
||||||
|
label="Method"
|
||||||
|
onChange={(e) => onMethodChange(e.target.value as HttpMethod)}
|
||||||
|
sx={{
|
||||||
|
width: "8rem",
|
||||||
|
".MuiOutlinedInput-notchedOutline": {
|
||||||
|
borderRightWidth: 0,
|
||||||
|
borderTopRightRadius: 0,
|
||||||
|
borderBottomRightRadius: 0,
|
||||||
|
},
|
||||||
|
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||||
|
borderRightWidth: 1,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.values(HttpMethod).map((method) => (
|
||||||
|
<MenuItem key={method} value={method}>
|
||||||
|
{method}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<TextField
|
||||||
|
label="URL"
|
||||||
|
placeholder="E.g. “https://example.com/foobar”"
|
||||||
|
value={url}
|
||||||
|
onChange={(e) => onUrlChange(e.target.value)}
|
||||||
|
required
|
||||||
|
variant="outlined"
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true,
|
||||||
|
}}
|
||||||
|
InputProps={{
|
||||||
|
sx: {
|
||||||
|
".MuiOutlinedInput-notchedOutline": {
|
||||||
|
borderRadius: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sx={{ flexGrow: 1 }}
|
||||||
|
/>
|
||||||
|
<FormControl>
|
||||||
|
<InputLabel id="req-proto-label">Protocol</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="req-proto-label"
|
||||||
|
id="req-proto"
|
||||||
|
value={proto}
|
||||||
|
label="Protocol"
|
||||||
|
onChange={(e) => onProtoChange(e.target.value as HttpProto)}
|
||||||
|
sx={{
|
||||||
|
".MuiOutlinedInput-notchedOutline": {
|
||||||
|
borderLeftWidth: 0,
|
||||||
|
borderTopLeftRadius: 0,
|
||||||
|
borderBottomLeftRadius: 0,
|
||||||
|
},
|
||||||
|
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||||
|
borderLeftWidth: 1,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.values(HttpProto).map((proto) => (
|
||||||
|
<MenuItem key={proto} value={proto}>
|
||||||
|
{proto}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditRequest;
|
91
admin/src/components/sender/EditRequestTabs.tsx
Normal file
91
admin/src/components/sender/EditRequestTabs.tsx
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import { TabContext, TabList, TabPanel } from "@mui/lab";
|
||||||
|
import { Box, Tab } from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import Editor from "../common/Editor";
|
||||||
|
|
||||||
|
import KeyValuePairTable, { KeyValuePair, KeyValuePairTableProps } from "./KeyValuePair";
|
||||||
|
|
||||||
|
enum TabValue {
|
||||||
|
QueryParams = "queryParams",
|
||||||
|
Headers = "headers",
|
||||||
|
Body = "body",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EditRequestTabsProps = {
|
||||||
|
queryParams: KeyValuePair[];
|
||||||
|
headers: KeyValuePair[];
|
||||||
|
onQueryParamChange: KeyValuePairTableProps["onChange"];
|
||||||
|
onQueryParamDelete: KeyValuePairTableProps["onDelete"];
|
||||||
|
onHeaderChange: KeyValuePairTableProps["onChange"];
|
||||||
|
onHeaderDelete: KeyValuePairTableProps["onDelete"];
|
||||||
|
body: string;
|
||||||
|
onBodyChange: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function EditRequestTabs(props: EditRequestTabsProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
queryParams,
|
||||||
|
onQueryParamChange,
|
||||||
|
onQueryParamDelete,
|
||||||
|
headers,
|
||||||
|
onHeaderChange,
|
||||||
|
onHeaderDelete,
|
||||||
|
body,
|
||||||
|
onBodyChange,
|
||||||
|
} = props;
|
||||||
|
const [tabValue, setTabValue] = useState(TabValue.QueryParams);
|
||||||
|
|
||||||
|
const tabSx = {
|
||||||
|
textTransform: "none",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box height="100%" sx={{ display: "flex", flexDirection: "column" }}>
|
||||||
|
<TabContext value={tabValue}>
|
||||||
|
<Box sx={{ borderBottom: 1, borderColor: "divider", mb: 2 }}>
|
||||||
|
<TabList onChange={(_, value) => setTabValue(value)}>
|
||||||
|
<Tab
|
||||||
|
value={TabValue.QueryParams}
|
||||||
|
label={"Query Params" + (queryParams.length - 1 ? ` (${queryParams.length - 1})` : "")}
|
||||||
|
sx={tabSx}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
value={TabValue.Headers}
|
||||||
|
label={"Headers" + (headers.length - 1 ? ` (${headers.length - 1})` : "")}
|
||||||
|
sx={tabSx}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
value={TabValue.Body}
|
||||||
|
label={"Body" + (body.length ? ` (${body.length} byte` + (body.length > 1 ? "s" : "") + ")" : "")}
|
||||||
|
sx={tabSx}
|
||||||
|
/>
|
||||||
|
</TabList>
|
||||||
|
</Box>
|
||||||
|
<Box flex="1 auto" overflow="hidden">
|
||||||
|
<TabPanel value={TabValue.QueryParams} sx={{ p: 0, height: "100%", overflow: "scroll" }}>
|
||||||
|
<Box>
|
||||||
|
<KeyValuePairTable items={queryParams} onChange={onQueryParamChange} onDelete={onQueryParamDelete} />
|
||||||
|
</Box>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={TabValue.Headers} sx={{ p: 0, height: "100%", overflow: "scroll" }}>
|
||||||
|
<Box>
|
||||||
|
<KeyValuePairTable items={headers} onChange={onHeaderChange} onDelete={onHeaderDelete} />
|
||||||
|
</Box>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={TabValue.Body} sx={{ p: 0, height: "100%" }}>
|
||||||
|
<Editor
|
||||||
|
content={body}
|
||||||
|
onChange={(value) => {
|
||||||
|
onBodyChange(value || "");
|
||||||
|
}}
|
||||||
|
monacoOptions={{ readOnly: false }}
|
||||||
|
contentType={headers.find(({ key }) => key.toLowerCase() === "content-type")?.value}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
</Box>
|
||||||
|
</TabContext>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditRequestTabs;
|
93
admin/src/components/sender/History.tsx
Normal file
93
admin/src/components/sender/History.tsx
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import { TableContainer, Table, TableHead, TableRow, TableCell, Typography, Box, TableBody } from "@mui/material";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useGetSenderRequestsQuery } from "../../generated/graphql";
|
||||||
|
import CenteredPaper from "../common/CenteredPaper";
|
||||||
|
import HttpStatusIcon from "../common/HttpStatusIcon";
|
||||||
|
|
||||||
|
function History(): JSX.Element {
|
||||||
|
const { data, loading } = useGetSenderRequestsQuery({
|
||||||
|
pollInterval: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const activeId = router.query.id as string | undefined;
|
||||||
|
|
||||||
|
const handleRowClick = (id: string) => {
|
||||||
|
router.push(`/sender?id=${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{!loading && data?.senderRequests && data?.senderRequests.length > 0 && (
|
||||||
|
<TableContainer sx={{ overflowX: "initial" }}>
|
||||||
|
<Table size="small" stickyHeader>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Method</TableCell>
|
||||||
|
<TableCell>Origin</TableCell>
|
||||||
|
<TableCell>Path</TableCell>
|
||||||
|
<TableCell>Status</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{data?.senderRequests &&
|
||||||
|
data.senderRequests.map(({ id, method, url, response }) => {
|
||||||
|
const { origin, pathname, search, hash } = new URL(url);
|
||||||
|
|
||||||
|
const cellStyle = {
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
key={id}
|
||||||
|
sx={{
|
||||||
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
},
|
||||||
|
...(id === activeId && {
|
||||||
|
bgcolor: "action.selected",
|
||||||
|
cursor: "inherit",
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
hover
|
||||||
|
onClick={() => handleRowClick(id)}
|
||||||
|
>
|
||||||
|
<TableCell style={{ ...cellStyle, width: "100px" }}>
|
||||||
|
<code>{method}</code>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ ...cellStyle, maxWidth: "100px" }}>{origin}</TableCell>
|
||||||
|
<TableCell sx={{ ...cellStyle, maxWidth: "200px" }}>
|
||||||
|
{decodeURIComponent(pathname + search + hash)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell style={{ maxWidth: "100px" }}>
|
||||||
|
{response && (
|
||||||
|
<div>
|
||||||
|
<HttpStatusIcon status={response.statusCode} />{" "}
|
||||||
|
<code>
|
||||||
|
{response.statusCode} {response.statusReason}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
)}
|
||||||
|
<Box sx={{ mt: 2, height: "100%" }}>
|
||||||
|
{!loading && data?.senderRequests.length === 0 && (
|
||||||
|
<CenteredPaper>
|
||||||
|
<Typography>No requests created yet.</Typography>
|
||||||
|
</CenteredPaper>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default History;
|
130
admin/src/components/sender/KeyValuePair.tsx
Normal file
130
admin/src/components/sender/KeyValuePair.tsx
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
import { IconButton, InputBase, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material";
|
||||||
|
import ClearIcon from "@mui/icons-material/Clear";
|
||||||
|
|
||||||
|
export type KeyValuePair = {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type KeyValuePairTableProps = {
|
||||||
|
items: KeyValuePair[];
|
||||||
|
onChange?: (key: string, value: string, index: number) => void;
|
||||||
|
onDelete?: (index: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function KeyValuePairTable({ items, onChange, onDelete }: KeyValuePairTableProps): JSX.Element {
|
||||||
|
const inputSx = {
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
"&.MuiInputBase-root input": {
|
||||||
|
p: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Key</TableCell>
|
||||||
|
<TableCell>Value</TableCell>
|
||||||
|
{onDelete && <TableCell padding="checkbox"></TableCell>}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody
|
||||||
|
sx={{
|
||||||
|
"td, th, input": {
|
||||||
|
fontFamily: "'JetBrains Mono', monospace",
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
py: 0.2,
|
||||||
|
},
|
||||||
|
"td span, th span": {
|
||||||
|
display: "block",
|
||||||
|
py: 0.7,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{items.map(({ key, value }, idx) => (
|
||||||
|
<TableRow
|
||||||
|
key={idx}
|
||||||
|
hover
|
||||||
|
sx={{
|
||||||
|
"& .delete-button": {
|
||||||
|
visibility: "hidden",
|
||||||
|
},
|
||||||
|
"&:hover .delete-button": {
|
||||||
|
visibility: "inherit",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableCell component="th" scope="row">
|
||||||
|
{!onChange && <span>{key}</span>}
|
||||||
|
{onChange && (
|
||||||
|
<InputBase
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
placeholder="Key"
|
||||||
|
value={key}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange && onChange(e.target.value, value, idx);
|
||||||
|
}}
|
||||||
|
sx={inputSx}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ width: "60%", wordBreak: "break-all" }}>
|
||||||
|
{!onChange && value}
|
||||||
|
{onChange && (
|
||||||
|
<InputBase
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
placeholder="Value"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange && onChange(key, e.target.value, idx);
|
||||||
|
}}
|
||||||
|
sx={inputSx}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
{onDelete && (
|
||||||
|
<TableCell>
|
||||||
|
<div className="delete-button">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
onDelete && onDelete(idx);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
visibility: onDelete === undefined || items.length === idx + 1 ? "hidden" : "inherit",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ClearIcon fontSize="inherit" />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortKeyValuePairs(items: KeyValuePair[]): KeyValuePair[] {
|
||||||
|
const sorted = [...items];
|
||||||
|
|
||||||
|
sorted.sort((a, b) => {
|
||||||
|
if (a.key < b.key) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.key > b.key) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KeyValuePairTable;
|
40
admin/src/components/sender/Response.tsx
Normal file
40
admin/src/components/sender/Response.tsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { Box, Typography } from "@mui/material";
|
||||||
|
|
||||||
|
import { sortKeyValuePairs } from "./KeyValuePair";
|
||||||
|
import ResponseTabs from "./ResponseTabs";
|
||||||
|
import ResponseStatus from "../common/ResponseStatus";
|
||||||
|
import { HttpResponseLog } from "../../generated/graphql";
|
||||||
|
|
||||||
|
export type ResponseProps = {
|
||||||
|
response?: HttpResponseLog | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Response({ response }: ResponseProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Box height="100%">
|
||||||
|
<div>
|
||||||
|
<Box sx={{ position: "absolute", right: 0, mt: 1.4 }}>
|
||||||
|
<Typography variant="overline" color="textSecondary" sx={{ float: "right", ml: 3 }}>
|
||||||
|
Response
|
||||||
|
</Typography>
|
||||||
|
{response && (
|
||||||
|
<Box sx={{ float: "right", mt: 0.2 }}>
|
||||||
|
<ResponseStatus
|
||||||
|
proto={response.proto}
|
||||||
|
statusCode={response.statusCode}
|
||||||
|
statusReason={response.statusReason}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
<ResponseTabs
|
||||||
|
body={response?.body}
|
||||||
|
headers={sortKeyValuePairs(response?.headers || [])}
|
||||||
|
hasResponse={response !== undefined && response !== null}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Response;
|
69
admin/src/components/sender/ResponseTabs.tsx
Normal file
69
admin/src/components/sender/ResponseTabs.tsx
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import { TabContext, TabList, TabPanel } from "@mui/lab";
|
||||||
|
import { Box, Tab, Typography } from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { HttpResponseLog } from "../../generated/graphql";
|
||||||
|
import CenteredPaper from "../common/CenteredPaper";
|
||||||
|
import Editor from "../common/Editor";
|
||||||
|
|
||||||
|
import KeyValuePairTable from "./KeyValuePair";
|
||||||
|
|
||||||
|
export type ResponseTabsProps = {
|
||||||
|
headers: HttpResponseLog["headers"];
|
||||||
|
body: HttpResponseLog["body"];
|
||||||
|
hasResponse: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum TabValue {
|
||||||
|
Body = "body",
|
||||||
|
Headers = "headers",
|
||||||
|
}
|
||||||
|
|
||||||
|
const reqNotSent = (
|
||||||
|
<CenteredPaper>
|
||||||
|
<Typography>Response not received yet.</Typography>
|
||||||
|
</CenteredPaper>
|
||||||
|
);
|
||||||
|
|
||||||
|
function ResponseTabs(props: ResponseTabsProps): JSX.Element {
|
||||||
|
const { headers, body, hasResponse } = props;
|
||||||
|
const [tabValue, setTabValue] = useState(TabValue.Body);
|
||||||
|
|
||||||
|
const contentType = headers.find((header) => header.key.toLowerCase() === "content-type")?.value;
|
||||||
|
|
||||||
|
const tabSx = {
|
||||||
|
textTransform: "none",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box height="100%" sx={{ display: "flex", flexDirection: "column" }}>
|
||||||
|
<TabContext value={tabValue}>
|
||||||
|
<Box sx={{ borderBottom: 1, borderColor: "divider", mb: 2 }}>
|
||||||
|
<TabList onChange={(_, value) => setTabValue(value)}>
|
||||||
|
<Tab
|
||||||
|
value={TabValue.Body}
|
||||||
|
label={"Body" + (body?.length ? ` (${body.length} byte` + (body.length > 1 ? "s" : "") + ")" : "")}
|
||||||
|
sx={tabSx}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
value={TabValue.Headers}
|
||||||
|
label={"Headers" + (headers.length ? ` (${headers.length})` : "")}
|
||||||
|
sx={tabSx}
|
||||||
|
/>
|
||||||
|
</TabList>
|
||||||
|
</Box>
|
||||||
|
<Box flex="1 auto" overflow="hidden">
|
||||||
|
<TabPanel value={TabValue.Body} sx={{ p: 0, height: "100%" }}>
|
||||||
|
{body && <Editor content={body} contentType={contentType} />}
|
||||||
|
{!hasResponse && reqNotSent}
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={TabValue.Headers} sx={{ p: 0, height: "100%", overflow: "scroll" }}>
|
||||||
|
{headers.length > 0 && <KeyValuePairTable items={headers} />}
|
||||||
|
{!hasResponse && reqNotSent}
|
||||||
|
</TabPanel>
|
||||||
|
</Box>
|
||||||
|
</TabContext>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResponseTabs;
|
|
@ -0,0 +1,5 @@
|
||||||
|
mutation CreateOrUpdateSenderRequest($request: SenderRequestInput!) {
|
||||||
|
createOrUpdateSenderRequest(request: $request) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
mutation CreateSenderRequestFromHttpRequestLog($id: ID!) {
|
||||||
|
createSenderRequestFromHttpRequestLog(id: $id) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
5
admin/src/components/sender/sendRequest.graphql
Normal file
5
admin/src/components/sender/sendRequest.graphql
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
mutation SendRequest($id: ID!) {
|
||||||
|
sendRequest(id: $id) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
26
admin/src/components/sender/senderRequest.graphql
Normal file
26
admin/src/components/sender/senderRequest.graphql
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
query GetSenderRequest($id: ID!) {
|
||||||
|
senderRequest(id: $id) {
|
||||||
|
id
|
||||||
|
sourceRequestLogID
|
||||||
|
url
|
||||||
|
method
|
||||||
|
proto
|
||||||
|
headers {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
body
|
||||||
|
timestamp
|
||||||
|
response {
|
||||||
|
id
|
||||||
|
proto
|
||||||
|
statusCode
|
||||||
|
statusReason
|
||||||
|
body
|
||||||
|
headers {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
admin/src/components/sender/senderRequests.graphql
Normal file
12
admin/src/components/sender/senderRequests.graphql
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
query GetSenderRequests {
|
||||||
|
senderRequests {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
method
|
||||||
|
response {
|
||||||
|
id
|
||||||
|
statusCode
|
||||||
|
statusReason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
479
admin/src/generated/graphql.tsx
Normal file
479
admin/src/generated/graphql.tsx
Normal file
|
@ -0,0 +1,479 @@
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import * as Apollo from '@apollo/client';
|
||||||
|
export type Maybe<T> = T | null;
|
||||||
|
export type InputMaybe<T> = Maybe<T>;
|
||||||
|
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||||
|
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
|
||||||
|
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
|
||||||
|
const defaultOptions = {} as const;
|
||||||
|
/** All built-in and custom scalars, mapped to their actual values */
|
||||||
|
export type Scalars = {
|
||||||
|
ID: string;
|
||||||
|
String: string;
|
||||||
|
Boolean: boolean;
|
||||||
|
Int: number;
|
||||||
|
Float: number;
|
||||||
|
Regexp: any;
|
||||||
|
Time: any;
|
||||||
|
URL: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClearHttpRequestLogResult = {
|
||||||
|
__typename?: 'ClearHTTPRequestLogResult';
|
||||||
|
success: Scalars['Boolean'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CloseProjectResult = {
|
||||||
|
__typename?: 'CloseProjectResult';
|
||||||
|
success: Scalars['Boolean'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteProjectResult = {
|
||||||
|
__typename?: 'DeleteProjectResult';
|
||||||
|
success: Scalars['Boolean'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteSenderRequestsResult = {
|
||||||
|
__typename?: 'DeleteSenderRequestsResult';
|
||||||
|
success: Scalars['Boolean'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HttpHeader = {
|
||||||
|
__typename?: 'HttpHeader';
|
||||||
|
key: Scalars['String'];
|
||||||
|
value: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HttpHeaderInput = {
|
||||||
|
key: Scalars['String'];
|
||||||
|
value: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum HttpMethod {
|
||||||
|
Connect = 'CONNECT',
|
||||||
|
Delete = 'DELETE',
|
||||||
|
Get = 'GET',
|
||||||
|
Head = 'HEAD',
|
||||||
|
Options = 'OPTIONS',
|
||||||
|
Patch = 'PATCH',
|
||||||
|
Post = 'POST',
|
||||||
|
Put = 'PUT',
|
||||||
|
Trace = 'TRACE'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum HttpProtocol {
|
||||||
|
Http1 = 'HTTP1',
|
||||||
|
Http2 = 'HTTP2'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HttpRequestLog = {
|
||||||
|
__typename?: 'HttpRequestLog';
|
||||||
|
body?: Maybe<Scalars['String']>;
|
||||||
|
headers: Array<HttpHeader>;
|
||||||
|
id: Scalars['ID'];
|
||||||
|
method: HttpMethod;
|
||||||
|
proto: Scalars['String'];
|
||||||
|
response?: Maybe<HttpResponseLog>;
|
||||||
|
timestamp: Scalars['Time'];
|
||||||
|
url: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HttpRequestLogFilter = {
|
||||||
|
__typename?: 'HttpRequestLogFilter';
|
||||||
|
onlyInScope: Scalars['Boolean'];
|
||||||
|
searchExpression?: Maybe<Scalars['String']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HttpRequestLogFilterInput = {
|
||||||
|
onlyInScope?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
searchExpression?: InputMaybe<Scalars['String']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HttpResponseLog = {
|
||||||
|
__typename?: 'HttpResponseLog';
|
||||||
|
body?: Maybe<Scalars['String']>;
|
||||||
|
headers: Array<HttpHeader>;
|
||||||
|
/** Will be the same ID as its related request ID. */
|
||||||
|
id: Scalars['ID'];
|
||||||
|
proto: HttpProtocol;
|
||||||
|
statusCode: Scalars['Int'];
|
||||||
|
statusReason: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Mutation = {
|
||||||
|
__typename?: 'Mutation';
|
||||||
|
clearHTTPRequestLog: ClearHttpRequestLogResult;
|
||||||
|
closeProject: CloseProjectResult;
|
||||||
|
createOrUpdateSenderRequest: SenderRequest;
|
||||||
|
createProject?: Maybe<Project>;
|
||||||
|
createSenderRequestFromHttpRequestLog: SenderRequest;
|
||||||
|
deleteProject: DeleteProjectResult;
|
||||||
|
deleteSenderRequests: DeleteSenderRequestsResult;
|
||||||
|
openProject?: Maybe<Project>;
|
||||||
|
sendRequest: SenderRequest;
|
||||||
|
setHttpRequestLogFilter?: Maybe<HttpRequestLogFilter>;
|
||||||
|
setScope: Array<ScopeRule>;
|
||||||
|
setSenderRequestFilter?: Maybe<SenderRequestFilter>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationCreateOrUpdateSenderRequestArgs = {
|
||||||
|
request: SenderRequestInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationCreateProjectArgs = {
|
||||||
|
name: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationCreateSenderRequestFromHttpRequestLogArgs = {
|
||||||
|
id: Scalars['ID'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationDeleteProjectArgs = {
|
||||||
|
id: Scalars['ID'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationOpenProjectArgs = {
|
||||||
|
id: Scalars['ID'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationSendRequestArgs = {
|
||||||
|
id: Scalars['ID'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationSetHttpRequestLogFilterArgs = {
|
||||||
|
filter?: InputMaybe<HttpRequestLogFilterInput>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationSetScopeArgs = {
|
||||||
|
scope: Array<ScopeRuleInput>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationSetSenderRequestFilterArgs = {
|
||||||
|
filter?: InputMaybe<SenderRequestFilterInput>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Project = {
|
||||||
|
__typename?: 'Project';
|
||||||
|
id: Scalars['ID'];
|
||||||
|
isActive: Scalars['Boolean'];
|
||||||
|
name: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Query = {
|
||||||
|
__typename?: 'Query';
|
||||||
|
activeProject?: Maybe<Project>;
|
||||||
|
httpRequestLog?: Maybe<HttpRequestLog>;
|
||||||
|
httpRequestLogFilter?: Maybe<HttpRequestLogFilter>;
|
||||||
|
httpRequestLogs: Array<HttpRequestLog>;
|
||||||
|
projects: Array<Project>;
|
||||||
|
scope: Array<ScopeRule>;
|
||||||
|
senderRequest?: Maybe<SenderRequest>;
|
||||||
|
senderRequests: Array<SenderRequest>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryHttpRequestLogArgs = {
|
||||||
|
id: Scalars['ID'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QuerySenderRequestArgs = {
|
||||||
|
id: Scalars['ID'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScopeHeader = {
|
||||||
|
__typename?: 'ScopeHeader';
|
||||||
|
key?: Maybe<Scalars['Regexp']>;
|
||||||
|
value?: Maybe<Scalars['Regexp']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScopeHeaderInput = {
|
||||||
|
key?: InputMaybe<Scalars['Regexp']>;
|
||||||
|
value?: InputMaybe<Scalars['Regexp']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScopeRule = {
|
||||||
|
__typename?: 'ScopeRule';
|
||||||
|
body?: Maybe<Scalars['Regexp']>;
|
||||||
|
header?: Maybe<ScopeHeader>;
|
||||||
|
url?: Maybe<Scalars['Regexp']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScopeRuleInput = {
|
||||||
|
body?: InputMaybe<Scalars['Regexp']>;
|
||||||
|
header?: InputMaybe<ScopeHeaderInput>;
|
||||||
|
url?: InputMaybe<Scalars['Regexp']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SenderRequest = {
|
||||||
|
__typename?: 'SenderRequest';
|
||||||
|
body?: Maybe<Scalars['String']>;
|
||||||
|
headers?: Maybe<Array<HttpHeader>>;
|
||||||
|
id: Scalars['ID'];
|
||||||
|
method: HttpMethod;
|
||||||
|
proto: HttpProtocol;
|
||||||
|
response?: Maybe<HttpResponseLog>;
|
||||||
|
sourceRequestLogID?: Maybe<Scalars['ID']>;
|
||||||
|
timestamp: Scalars['Time'];
|
||||||
|
url: Scalars['URL'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SenderRequestFilter = {
|
||||||
|
__typename?: 'SenderRequestFilter';
|
||||||
|
onlyInScope: Scalars['Boolean'];
|
||||||
|
searchExpression?: Maybe<Scalars['String']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SenderRequestFilterInput = {
|
||||||
|
onlyInScope?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
searchExpression?: InputMaybe<Scalars['String']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SenderRequestInput = {
|
||||||
|
body?: InputMaybe<Scalars['String']>;
|
||||||
|
headers?: InputMaybe<Array<HttpHeaderInput>>;
|
||||||
|
id?: InputMaybe<Scalars['ID']>;
|
||||||
|
method?: InputMaybe<HttpMethod>;
|
||||||
|
proto?: InputMaybe<HttpProtocol>;
|
||||||
|
url: Scalars['URL'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateOrUpdateSenderRequestMutationVariables = Exact<{
|
||||||
|
request: SenderRequestInput;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type CreateOrUpdateSenderRequestMutation = { __typename?: 'Mutation', createOrUpdateSenderRequest: { __typename?: 'SenderRequest', id: string } };
|
||||||
|
|
||||||
|
export type CreateSenderRequestFromHttpRequestLogMutationVariables = Exact<{
|
||||||
|
id: Scalars['ID'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type CreateSenderRequestFromHttpRequestLogMutation = { __typename?: 'Mutation', createSenderRequestFromHttpRequestLog: { __typename?: 'SenderRequest', id: string } };
|
||||||
|
|
||||||
|
export type SendRequestMutationVariables = Exact<{
|
||||||
|
id: Scalars['ID'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type SendRequestMutation = { __typename?: 'Mutation', sendRequest: { __typename?: 'SenderRequest', id: string } };
|
||||||
|
|
||||||
|
export type GetSenderRequestQueryVariables = Exact<{
|
||||||
|
id: Scalars['ID'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type GetSenderRequestQuery = { __typename?: 'Query', senderRequest?: { __typename?: 'SenderRequest', id: string, sourceRequestLogID?: string | null, url: any, method: HttpMethod, proto: HttpProtocol, body?: string | null, timestamp: any, headers?: Array<{ __typename?: 'HttpHeader', key: string, value: string }> | null, response?: { __typename?: 'HttpResponseLog', id: string, proto: HttpProtocol, statusCode: number, statusReason: string, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }> } | null } | null };
|
||||||
|
|
||||||
|
export type GetSenderRequestsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
|
export type GetSenderRequestsQuery = { __typename?: 'Query', senderRequests: Array<{ __typename?: 'SenderRequest', id: string, url: any, method: HttpMethod, response?: { __typename?: 'HttpResponseLog', id: string, statusCode: number, statusReason: string } | null }> };
|
||||||
|
|
||||||
|
|
||||||
|
export const CreateOrUpdateSenderRequestDocument = gql`
|
||||||
|
mutation CreateOrUpdateSenderRequest($request: SenderRequestInput!) {
|
||||||
|
createOrUpdateSenderRequest(request: $request) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type CreateOrUpdateSenderRequestMutationFn = Apollo.MutationFunction<CreateOrUpdateSenderRequestMutation, CreateOrUpdateSenderRequestMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useCreateOrUpdateSenderRequestMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useCreateOrUpdateSenderRequestMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useCreateOrUpdateSenderRequestMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [createOrUpdateSenderRequestMutation, { data, loading, error }] = useCreateOrUpdateSenderRequestMutation({
|
||||||
|
* variables: {
|
||||||
|
* request: // value for 'request'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useCreateOrUpdateSenderRequestMutation(baseOptions?: Apollo.MutationHookOptions<CreateOrUpdateSenderRequestMutation, CreateOrUpdateSenderRequestMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<CreateOrUpdateSenderRequestMutation, CreateOrUpdateSenderRequestMutationVariables>(CreateOrUpdateSenderRequestDocument, options);
|
||||||
|
}
|
||||||
|
export type CreateOrUpdateSenderRequestMutationHookResult = ReturnType<typeof useCreateOrUpdateSenderRequestMutation>;
|
||||||
|
export type CreateOrUpdateSenderRequestMutationResult = Apollo.MutationResult<CreateOrUpdateSenderRequestMutation>;
|
||||||
|
export type CreateOrUpdateSenderRequestMutationOptions = Apollo.BaseMutationOptions<CreateOrUpdateSenderRequestMutation, CreateOrUpdateSenderRequestMutationVariables>;
|
||||||
|
export const CreateSenderRequestFromHttpRequestLogDocument = gql`
|
||||||
|
mutation CreateSenderRequestFromHttpRequestLog($id: ID!) {
|
||||||
|
createSenderRequestFromHttpRequestLog(id: $id) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type CreateSenderRequestFromHttpRequestLogMutationFn = Apollo.MutationFunction<CreateSenderRequestFromHttpRequestLogMutation, CreateSenderRequestFromHttpRequestLogMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useCreateSenderRequestFromHttpRequestLogMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useCreateSenderRequestFromHttpRequestLogMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useCreateSenderRequestFromHttpRequestLogMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [createSenderRequestFromHttpRequestLogMutation, { data, loading, error }] = useCreateSenderRequestFromHttpRequestLogMutation({
|
||||||
|
* variables: {
|
||||||
|
* id: // value for 'id'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useCreateSenderRequestFromHttpRequestLogMutation(baseOptions?: Apollo.MutationHookOptions<CreateSenderRequestFromHttpRequestLogMutation, CreateSenderRequestFromHttpRequestLogMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<CreateSenderRequestFromHttpRequestLogMutation, CreateSenderRequestFromHttpRequestLogMutationVariables>(CreateSenderRequestFromHttpRequestLogDocument, options);
|
||||||
|
}
|
||||||
|
export type CreateSenderRequestFromHttpRequestLogMutationHookResult = ReturnType<typeof useCreateSenderRequestFromHttpRequestLogMutation>;
|
||||||
|
export type CreateSenderRequestFromHttpRequestLogMutationResult = Apollo.MutationResult<CreateSenderRequestFromHttpRequestLogMutation>;
|
||||||
|
export type CreateSenderRequestFromHttpRequestLogMutationOptions = Apollo.BaseMutationOptions<CreateSenderRequestFromHttpRequestLogMutation, CreateSenderRequestFromHttpRequestLogMutationVariables>;
|
||||||
|
export const SendRequestDocument = gql`
|
||||||
|
mutation SendRequest($id: ID!) {
|
||||||
|
sendRequest(id: $id) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type SendRequestMutationFn = Apollo.MutationFunction<SendRequestMutation, SendRequestMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useSendRequestMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useSendRequestMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useSendRequestMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [sendRequestMutation, { data, loading, error }] = useSendRequestMutation({
|
||||||
|
* variables: {
|
||||||
|
* id: // value for 'id'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useSendRequestMutation(baseOptions?: Apollo.MutationHookOptions<SendRequestMutation, SendRequestMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<SendRequestMutation, SendRequestMutationVariables>(SendRequestDocument, options);
|
||||||
|
}
|
||||||
|
export type SendRequestMutationHookResult = ReturnType<typeof useSendRequestMutation>;
|
||||||
|
export type SendRequestMutationResult = Apollo.MutationResult<SendRequestMutation>;
|
||||||
|
export type SendRequestMutationOptions = Apollo.BaseMutationOptions<SendRequestMutation, SendRequestMutationVariables>;
|
||||||
|
export const GetSenderRequestDocument = gql`
|
||||||
|
query GetSenderRequest($id: ID!) {
|
||||||
|
senderRequest(id: $id) {
|
||||||
|
id
|
||||||
|
sourceRequestLogID
|
||||||
|
url
|
||||||
|
method
|
||||||
|
proto
|
||||||
|
headers {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
body
|
||||||
|
timestamp
|
||||||
|
response {
|
||||||
|
id
|
||||||
|
proto
|
||||||
|
statusCode
|
||||||
|
statusReason
|
||||||
|
body
|
||||||
|
headers {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useGetSenderRequestQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a React component, call `useGetSenderRequestQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useGetSenderRequestQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||||
|
* you can use to render your UI.
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { data, loading, error } = useGetSenderRequestQuery({
|
||||||
|
* variables: {
|
||||||
|
* id: // value for 'id'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useGetSenderRequestQuery(baseOptions: Apollo.QueryHookOptions<GetSenderRequestQuery, GetSenderRequestQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useQuery<GetSenderRequestQuery, GetSenderRequestQueryVariables>(GetSenderRequestDocument, options);
|
||||||
|
}
|
||||||
|
export function useGetSenderRequestLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetSenderRequestQuery, GetSenderRequestQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useLazyQuery<GetSenderRequestQuery, GetSenderRequestQueryVariables>(GetSenderRequestDocument, options);
|
||||||
|
}
|
||||||
|
export type GetSenderRequestQueryHookResult = ReturnType<typeof useGetSenderRequestQuery>;
|
||||||
|
export type GetSenderRequestLazyQueryHookResult = ReturnType<typeof useGetSenderRequestLazyQuery>;
|
||||||
|
export type GetSenderRequestQueryResult = Apollo.QueryResult<GetSenderRequestQuery, GetSenderRequestQueryVariables>;
|
||||||
|
export const GetSenderRequestsDocument = gql`
|
||||||
|
query GetSenderRequests {
|
||||||
|
senderRequests {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
method
|
||||||
|
response {
|
||||||
|
id
|
||||||
|
statusCode
|
||||||
|
statusReason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useGetSenderRequestsQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a React component, call `useGetSenderRequestsQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useGetSenderRequestsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||||
|
* you can use to render your UI.
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { data, loading, error } = useGetSenderRequestsQuery({
|
||||||
|
* variables: {
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useGetSenderRequestsQuery(baseOptions?: Apollo.QueryHookOptions<GetSenderRequestsQuery, GetSenderRequestsQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useQuery<GetSenderRequestsQuery, GetSenderRequestsQueryVariables>(GetSenderRequestsDocument, options);
|
||||||
|
}
|
||||||
|
export function useGetSenderRequestsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetSenderRequestsQuery, GetSenderRequestsQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useLazyQuery<GetSenderRequestsQuery, GetSenderRequestsQueryVariables>(GetSenderRequestsDocument, options);
|
||||||
|
}
|
||||||
|
export type GetSenderRequestsQueryHookResult = ReturnType<typeof useGetSenderRequestsQuery>;
|
||||||
|
export type GetSenderRequestsLazyQueryHookResult = ReturnType<typeof useGetSenderRequestsLazyQuery>;
|
||||||
|
export type GetSenderRequestsQueryResult = Apollo.QueryResult<GetSenderRequestsQuery, GetSenderRequestsQueryVariables>;
|
|
@ -9,6 +9,7 @@ import { CacheProvider, EmotionCache } from "@emotion/react";
|
||||||
import createEmotionCache from "../lib/createEmotionCache";
|
import createEmotionCache from "../lib/createEmotionCache";
|
||||||
import theme from "../lib/theme";
|
import theme from "../lib/theme";
|
||||||
import { useApollo } from "../lib/graphql";
|
import { useApollo } from "../lib/graphql";
|
||||||
|
import "../styles.css";
|
||||||
|
|
||||||
// Client-side cache, shared for the whole session of the user in the browser.
|
// Client-side cache, shared for the whole session of the user in the browser.
|
||||||
const clientSideEmotionCache = createEmotionCache();
|
const clientSideEmotionCache = createEmotionCache();
|
||||||
|
|
|
@ -1,11 +1,47 @@
|
||||||
import { Box, Typography } from "@mui/material";
|
import { Box } from "@mui/system";
|
||||||
|
import { AllotmentProps } from "allotment";
|
||||||
|
import { PaneProps } from "allotment/dist/types/src/allotment";
|
||||||
|
import { ComponentType, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import Layout, { Page } from "../../components/Layout";
|
import Layout, { Page } from "../../components/Layout";
|
||||||
|
import EditRequest from "../../components/sender/EditRequest";
|
||||||
|
import History from "../../components/sender/History";
|
||||||
|
|
||||||
function Index(): JSX.Element {
|
function Index(): JSX.Element {
|
||||||
|
const isMountedRef = useRef(false);
|
||||||
|
const [Allotment, setAllotment] = useState<
|
||||||
|
(ComponentType<AllotmentProps> & { Pane: ComponentType<PaneProps> }) | null
|
||||||
|
>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
isMountedRef.current = true;
|
||||||
|
import("allotment")
|
||||||
|
.then((mod) => {
|
||||||
|
if (!isMountedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAllotment(mod.Allotment);
|
||||||
|
})
|
||||||
|
.catch((err) => console.error(err, `could not import allotment ${err.message}`));
|
||||||
|
return () => {
|
||||||
|
isMountedRef.current = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
if (!Allotment) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout page={Page.Sender} title="Sender">
|
<Layout page={Page.Sender} title="Sender">
|
||||||
<Typography paragraph>Coming soon…</Typography>
|
<Allotment vertical={true} defaultSizes={[70, 30]}>
|
||||||
|
<Box sx={{ pt: 0.75, height: "100%" }}>
|
||||||
|
<EditRequest />
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ height: "100%", py: 2, overflow: "hidden" }}>
|
||||||
|
<Box sx={{ height: "100%", overflow: "scroll" }}>
|
||||||
|
<History />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Allotment>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
5
admin/src/styles.css
Normal file
5
admin/src/styles.css
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#__next {
|
||||||
|
height: 100%;
|
||||||
|
}
|
|
@ -6,6 +6,7 @@
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
"esnext"
|
"esnext"
|
||||||
],
|
],
|
||||||
|
"downlevelIteration": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|
2870
admin/yarn.lock
2870
admin/yarn.lock
File diff suppressed because it is too large
Load diff
|
@ -25,6 +25,7 @@ import (
|
||||||
"github.com/dstotijn/hetty/pkg/proxy"
|
"github.com/dstotijn/hetty/pkg/proxy"
|
||||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
"github.com/dstotijn/hetty/pkg/scope"
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
|
"github.com/dstotijn/hetty/pkg/sender"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "0.0.0"
|
var version = "0.0.0"
|
||||||
|
@ -94,9 +95,15 @@ func run() error {
|
||||||
Repository: badger,
|
Repository: badger,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
senderService := sender.NewService(sender.Config{
|
||||||
|
Repository: badger,
|
||||||
|
ReqLogService: reqLogService,
|
||||||
|
})
|
||||||
|
|
||||||
projService, err := proj.NewService(proj.Config{
|
projService, err := proj.NewService(proj.Config{
|
||||||
Repository: badger,
|
Repository: badger,
|
||||||
ReqLogService: reqLogService,
|
ReqLogService: reqLogService,
|
||||||
|
SenderService: senderService,
|
||||||
Scope: scope,
|
Scope: scope,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -128,8 +135,9 @@ func run() error {
|
||||||
adminRouter.Path("/api/playground/").Handler(playground.Handler("GraphQL Playground", "/api/graphql/"))
|
adminRouter.Path("/api/playground/").Handler(playground.Handler("GraphQL Playground", "/api/graphql/"))
|
||||||
adminRouter.Path("/api/graphql/").Handler(
|
adminRouter.Path("/api/graphql/").Handler(
|
||||||
handler.NewDefaultServer(api.NewExecutableSchema(api.Config{Resolvers: &api.Resolver{
|
handler.NewDefaultServer(api.NewExecutableSchema(api.Config{Resolvers: &api.Resolver{
|
||||||
RequestLogService: reqLogService,
|
|
||||||
ProjectService: projService,
|
ProjectService: projService,
|
||||||
|
RequestLogService: reqLogService,
|
||||||
|
SenderService: senderService,
|
||||||
}})))
|
}})))
|
||||||
|
|
||||||
// Admin interface.
|
// Admin interface.
|
||||||
|
|
|
@ -46,6 +46,9 @@ models:
|
||||||
ID:
|
ID:
|
||||||
model:
|
model:
|
||||||
- github.com/dstotijn/hetty/pkg/api.ULID
|
- github.com/dstotijn/hetty/pkg/api.ULID
|
||||||
|
URL:
|
||||||
|
model:
|
||||||
|
- github.com/dstotijn/hetty/pkg/api.URL
|
||||||
# Int:
|
# Int:
|
||||||
# model:
|
# model:
|
||||||
# - github.com/99designs/gqlgen/graphql.Int
|
# - github.com/99designs/gqlgen/graphql.Int
|
||||||
|
|
1655
pkg/api/generated.go
1655
pkg/api/generated.go
File diff suppressed because it is too large
Load diff
|
@ -3,29 +3,49 @@ package api
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/99designs/gqlgen/graphql"
|
||||||
"github.com/oklog/ulid"
|
"github.com/oklog/ulid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ULID ulid.ULID
|
func MarshalULID(u ulid.ULID) graphql.Marshaler {
|
||||||
|
return graphql.WriterFunc(func(w io.Writer) {
|
||||||
|
fmt.Fprint(w, strconv.Quote(u.String()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ULID) UnmarshalGQL(v interface{}) (err error) {
|
func UnmarshalULID(v interface{}) (ulid.ULID, error) {
|
||||||
str, ok := v.(string)
|
rawULID, ok := v.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("ulid must be a string")
|
return ulid.ULID{}, fmt.Errorf("ulid must be a string")
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := ulid.Parse(str)
|
u, err := ulid.Parse(rawULID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse ULID: %w", err)
|
return ulid.ULID{}, fmt.Errorf("failed to parse ULID: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
*u = ULID(id)
|
return u, nil
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u ULID) MarshalGQL(w io.Writer) {
|
func MarshalURL(u *url.URL) graphql.Marshaler {
|
||||||
fmt.Fprint(w, strconv.Quote(ulid.ULID(u).String()))
|
return graphql.WriterFunc(func(w io.Writer) {
|
||||||
|
fmt.Fprint(w, strconv.Quote(u.String()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnmarshalURL(v interface{}) (*url.URL, error) {
|
||||||
|
rawURL, ok := v.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("url must be a string")
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,11 @@ package api
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClearHTTPRequestLogResult struct {
|
type ClearHTTPRequestLogResult struct {
|
||||||
|
@ -21,13 +24,22 @@ type DeleteProjectResult struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeleteSenderRequestsResult struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
|
||||||
type HTTPHeader struct {
|
type HTTPHeader struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HTTPHeaderInput struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
type HTTPRequestLog struct {
|
type HTTPRequestLog struct {
|
||||||
ID ULID `json:"id"`
|
ID ulid.ULID `json:"id"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Method HTTPMethod `json:"method"`
|
Method HTTPMethod `json:"method"`
|
||||||
Proto string `json:"proto"`
|
Proto string `json:"proto"`
|
||||||
|
@ -48,7 +60,9 @@ type HTTPRequestLogFilterInput struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTPResponseLog struct {
|
type HTTPResponseLog struct {
|
||||||
Proto string `json:"proto"`
|
// Will be the same ID as its related request ID.
|
||||||
|
ID ulid.ULID `json:"id"`
|
||||||
|
Proto HTTPProtocol `json:"proto"`
|
||||||
StatusCode int `json:"statusCode"`
|
StatusCode int `json:"statusCode"`
|
||||||
StatusReason string `json:"statusReason"`
|
StatusReason string `json:"statusReason"`
|
||||||
Body *string `json:"body"`
|
Body *string `json:"body"`
|
||||||
|
@ -56,7 +70,7 @@ type HTTPResponseLog struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Project struct {
|
type Project struct {
|
||||||
ID ULID `json:"id"`
|
ID ulid.ULID `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
IsActive bool `json:"isActive"`
|
IsActive bool `json:"isActive"`
|
||||||
}
|
}
|
||||||
|
@ -83,6 +97,37 @@ type ScopeRuleInput struct {
|
||||||
Body *string `json:"body"`
|
Body *string `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SenderRequest struct {
|
||||||
|
ID ulid.ULID `json:"id"`
|
||||||
|
SourceRequestLogID *ulid.ULID `json:"sourceRequestLogID"`
|
||||||
|
URL *url.URL `json:"url"`
|
||||||
|
Method HTTPMethod `json:"method"`
|
||||||
|
Proto HTTPProtocol `json:"proto"`
|
||||||
|
Headers []HTTPHeader `json:"headers"`
|
||||||
|
Body *string `json:"body"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Response *HTTPResponseLog `json:"response"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SenderRequestFilter struct {
|
||||||
|
OnlyInScope bool `json:"onlyInScope"`
|
||||||
|
SearchExpression *string `json:"searchExpression"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SenderRequestFilterInput struct {
|
||||||
|
OnlyInScope *bool `json:"onlyInScope"`
|
||||||
|
SearchExpression *string `json:"searchExpression"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SenderRequestInput struct {
|
||||||
|
ID *ulid.ULID `json:"id"`
|
||||||
|
URL *url.URL `json:"url"`
|
||||||
|
Method *HTTPMethod `json:"method"`
|
||||||
|
Proto *HTTPProtocol `json:"proto"`
|
||||||
|
Headers []HTTPHeaderInput `json:"headers"`
|
||||||
|
Body *string `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
type HTTPMethod string
|
type HTTPMethod string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -137,3 +182,44 @@ func (e *HTTPMethod) UnmarshalGQL(v interface{}) error {
|
||||||
func (e HTTPMethod) MarshalGQL(w io.Writer) {
|
func (e HTTPMethod) MarshalGQL(w io.Writer) {
|
||||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HTTPProtocol string
|
||||||
|
|
||||||
|
const (
|
||||||
|
HTTPProtocolHTTP1 HTTPProtocol = "HTTP1"
|
||||||
|
HTTPProtocolHTTP2 HTTPProtocol = "HTTP2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AllHTTPProtocol = []HTTPProtocol{
|
||||||
|
HTTPProtocolHTTP1,
|
||||||
|
HTTPProtocolHTTP2,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e HTTPProtocol) IsValid() bool {
|
||||||
|
switch e {
|
||||||
|
case HTTPProtocolHTTP1, HTTPProtocolHTTP2:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e HTTPProtocol) String() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HTTPProtocol) UnmarshalGQL(v interface{}) error {
|
||||||
|
str, ok := v.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("enums must be strings")
|
||||||
|
}
|
||||||
|
|
||||||
|
*e = HTTPProtocol(str)
|
||||||
|
if !e.IsValid() {
|
||||||
|
return fmt.Errorf("%s is not a valid HttpProtocol", str)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e HTTPProtocol) MarshalGQL(w io.Writer) {
|
||||||
|
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -17,11 +18,23 @@ import (
|
||||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
"github.com/dstotijn/hetty/pkg/scope"
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
"github.com/dstotijn/hetty/pkg/search"
|
"github.com/dstotijn/hetty/pkg/search"
|
||||||
|
"github.com/dstotijn/hetty/pkg/sender"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var httpProtocolMap = map[string]HTTPProtocol{
|
||||||
|
sender.HTTPProto1: HTTPProtocolHTTP1,
|
||||||
|
sender.HTTPProto2: HTTPProtocolHTTP2,
|
||||||
|
}
|
||||||
|
|
||||||
|
var revHTTPProtocolMap = map[HTTPProtocol]string{
|
||||||
|
HTTPProtocolHTTP1: sender.HTTPProto1,
|
||||||
|
HTTPProtocolHTTP2: sender.HTTPProto2,
|
||||||
|
}
|
||||||
|
|
||||||
type Resolver struct {
|
type Resolver struct {
|
||||||
ProjectService proj.Service
|
ProjectService proj.Service
|
||||||
RequestLogService *reqlog.Service
|
RequestLogService reqlog.Service
|
||||||
|
SenderService sender.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -54,8 +67,8 @@ func (r *queryResolver) HTTPRequestLogs(ctx context.Context) ([]HTTPRequestLog,
|
||||||
return logs, nil
|
return logs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) HTTPRequestLog(ctx context.Context, id ULID) (*HTTPRequestLog, error) {
|
func (r *queryResolver) HTTPRequestLog(ctx context.Context, id ulid.ULID) (*HTTPRequestLog, error) {
|
||||||
log, err := r.RequestLogService.FindRequestLogByID(ctx, ulid.ULID(id))
|
log, err := r.RequestLogService.FindRequestLogByID(ctx, id)
|
||||||
if errors.Is(err, reqlog.ErrRequestNotFound) {
|
if errors.Is(err, reqlog.ErrRequestNotFound) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
@ -77,7 +90,7 @@ func parseRequestLog(reqLog reqlog.RequestLog) (HTTPRequestLog, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
log := HTTPRequestLog{
|
log := HTTPRequestLog{
|
||||||
ID: ULID(reqLog.ID),
|
ID: reqLog.ID,
|
||||||
Proto: reqLog.Proto,
|
Proto: reqLog.Proto,
|
||||||
Method: method,
|
Method: method,
|
||||||
Timestamp: ulid.Time(reqLog.ID.Time()),
|
Timestamp: ulid.Time(reqLog.ID.Time()),
|
||||||
|
@ -106,36 +119,54 @@ func parseRequestLog(reqLog reqlog.RequestLog) (HTTPRequestLog, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if reqLog.Response != nil {
|
if reqLog.Response != nil {
|
||||||
log.Response = &HTTPResponseLog{
|
resLog, err := parseResponseLog(*reqLog.Response)
|
||||||
Proto: reqLog.Response.Proto,
|
if err != nil {
|
||||||
StatusCode: reqLog.Response.StatusCode,
|
return HTTPRequestLog{}, err
|
||||||
}
|
}
|
||||||
statusReasonSubs := strings.SplitN(reqLog.Response.Status, " ", 2)
|
|
||||||
|
resLog.ID = reqLog.ID
|
||||||
|
|
||||||
|
log.Response = &resLog
|
||||||
|
}
|
||||||
|
|
||||||
|
return log, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseResponseLog(resLog reqlog.ResponseLog) (HTTPResponseLog, error) {
|
||||||
|
proto := httpProtocolMap[resLog.Proto]
|
||||||
|
if !proto.IsValid() {
|
||||||
|
return HTTPResponseLog{}, fmt.Errorf("sender response has invalid protocol: %v", resLog.Proto)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpResLog := HTTPResponseLog{
|
||||||
|
Proto: proto,
|
||||||
|
StatusCode: resLog.StatusCode,
|
||||||
|
}
|
||||||
|
statusReasonSubs := strings.SplitN(resLog.Status, " ", 2)
|
||||||
|
|
||||||
if len(statusReasonSubs) == 2 {
|
if len(statusReasonSubs) == 2 {
|
||||||
log.Response.StatusReason = statusReasonSubs[1]
|
httpResLog.StatusReason = statusReasonSubs[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(reqLog.Response.Body) > 0 {
|
if len(resLog.Body) > 0 {
|
||||||
bodyStr := string(reqLog.Response.Body)
|
bodyStr := string(resLog.Body)
|
||||||
log.Response.Body = &bodyStr
|
httpResLog.Body = &bodyStr
|
||||||
}
|
}
|
||||||
|
|
||||||
if reqLog.Response.Header != nil {
|
if resLog.Header != nil {
|
||||||
log.Response.Headers = make([]HTTPHeader, 0)
|
httpResLog.Headers = make([]HTTPHeader, 0)
|
||||||
|
|
||||||
for key, values := range reqLog.Response.Header {
|
for key, values := range resLog.Header {
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
log.Response.Headers = append(log.Response.Headers, HTTPHeader{
|
httpResLog.Headers = append(httpResLog.Headers, HTTPHeader{
|
||||||
Key: key,
|
Key: key,
|
||||||
Value: value,
|
Value: value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return log, nil
|
return httpResLog, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) CreateProject(ctx context.Context, name string) (*Project, error) {
|
func (r *mutationResolver) CreateProject(ctx context.Context, name string) (*Project, error) {
|
||||||
|
@ -147,14 +178,14 @@ func (r *mutationResolver) CreateProject(ctx context.Context, name string) (*Pro
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Project{
|
return &Project{
|
||||||
ID: ULID(p.ID),
|
ID: p.ID,
|
||||||
Name: p.Name,
|
Name: p.Name,
|
||||||
IsActive: r.ProjectService.IsProjectActive(p.ID),
|
IsActive: r.ProjectService.IsProjectActive(p.ID),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) OpenProject(ctx context.Context, id ULID) (*Project, error) {
|
func (r *mutationResolver) OpenProject(ctx context.Context, id ulid.ULID) (*Project, error) {
|
||||||
p, err := r.ProjectService.OpenProject(ctx, ulid.ULID(id))
|
p, err := r.ProjectService.OpenProject(ctx, id)
|
||||||
if errors.Is(err, proj.ErrInvalidName) {
|
if errors.Is(err, proj.ErrInvalidName) {
|
||||||
return nil, gqlerror.Errorf("Project name must only contain alphanumeric or space chars.")
|
return nil, gqlerror.Errorf("Project name must only contain alphanumeric or space chars.")
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
@ -162,7 +193,7 @@ func (r *mutationResolver) OpenProject(ctx context.Context, id ULID) (*Project,
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Project{
|
return &Project{
|
||||||
ID: ULID(p.ID),
|
ID: p.ID,
|
||||||
Name: p.Name,
|
Name: p.Name,
|
||||||
IsActive: r.ProjectService.IsProjectActive(p.ID),
|
IsActive: r.ProjectService.IsProjectActive(p.ID),
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -177,7 +208,7 @@ func (r *queryResolver) ActiveProject(ctx context.Context) (*Project, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Project{
|
return &Project{
|
||||||
ID: ULID(p.ID),
|
ID: p.ID,
|
||||||
Name: p.Name,
|
Name: p.Name,
|
||||||
IsActive: r.ProjectService.IsProjectActive(p.ID),
|
IsActive: r.ProjectService.IsProjectActive(p.ID),
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -192,7 +223,7 @@ func (r *queryResolver) Projects(ctx context.Context) ([]Project, error) {
|
||||||
projects := make([]Project, len(p))
|
projects := make([]Project, len(p))
|
||||||
for i, proj := range p {
|
for i, proj := range p {
|
||||||
projects[i] = Project{
|
projects[i] = Project{
|
||||||
ID: ULID(proj.ID),
|
ID: proj.ID,
|
||||||
Name: proj.Name,
|
Name: proj.Name,
|
||||||
IsActive: r.ProjectService.IsProjectActive(proj.ID),
|
IsActive: r.ProjectService.IsProjectActive(proj.ID),
|
||||||
}
|
}
|
||||||
|
@ -224,8 +255,8 @@ func (r *mutationResolver) CloseProject(ctx context.Context) (*CloseProjectResul
|
||||||
return &CloseProjectResult{true}, nil
|
return &CloseProjectResult{true}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) DeleteProject(ctx context.Context, id ULID) (*DeleteProjectResult, error) {
|
func (r *mutationResolver) DeleteProject(ctx context.Context, id ulid.ULID) (*DeleteProjectResult, error) {
|
||||||
if err := r.ProjectService.DeleteProject(ctx, ulid.ULID(id)); err != nil {
|
if err := r.ProjectService.DeleteProject(ctx, id); err != nil {
|
||||||
return nil, fmt.Errorf("could not delete project: %w", err)
|
return nil, fmt.Errorf("could not delete project: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -296,7 +327,7 @@ func (r *mutationResolver) SetScope(ctx context.Context, input []ScopeRuleInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) HTTPRequestLogFilter(ctx context.Context) (*HTTPRequestLogFilter, error) {
|
func (r *queryResolver) HTTPRequestLogFilter(ctx context.Context) (*HTTPRequestLogFilter, error) {
|
||||||
return findReqFilterToHTTPReqLogFilter(r.RequestLogService.FindReqsFilter), nil
|
return findReqFilterToHTTPReqLogFilter(r.RequestLogService.FindReqsFilter()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) SetHTTPRequestLogFilter(
|
func (r *mutationResolver) SetHTTPRequestLogFilter(
|
||||||
|
@ -318,6 +349,221 @@ func (r *mutationResolver) SetHTTPRequestLogFilter(
|
||||||
return findReqFilterToHTTPReqLogFilter(filter), nil
|
return findReqFilterToHTTPReqLogFilter(filter), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) SenderRequest(ctx context.Context, id ulid.ULID) (*SenderRequest, error) {
|
||||||
|
senderReq, err := r.SenderService.FindRequestByID(ctx, id)
|
||||||
|
if errors.Is(err, sender.ErrRequestNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not get request by ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := parseSenderRequest(senderReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) SenderRequests(ctx context.Context) ([]SenderRequest, error) {
|
||||||
|
reqs, err := r.SenderService.FindRequests(ctx)
|
||||||
|
if errors.Is(err, proj.ErrNoProject) {
|
||||||
|
return nil, noActiveProjectErr(ctx)
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find sender requests: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
senderReqs := make([]SenderRequest, len(reqs))
|
||||||
|
|
||||||
|
for i, req := range reqs {
|
||||||
|
req, err := parseSenderRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
senderReqs[i] = req
|
||||||
|
}
|
||||||
|
|
||||||
|
return senderReqs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) SetSenderRequestFilter(
|
||||||
|
ctx context.Context,
|
||||||
|
input *SenderRequestFilterInput,
|
||||||
|
) (*SenderRequestFilter, error) {
|
||||||
|
filter, err := findSenderRequestsFilterFromInput(input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not parse request log filter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.ProjectService.SetSenderRequestFindFilter(ctx, filter)
|
||||||
|
if errors.Is(err, proj.ErrNoProject) {
|
||||||
|
return nil, noActiveProjectErr(ctx)
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not set request log filter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return findReqFilterToSenderReqFilter(filter), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) CreateOrUpdateSenderRequest(ctx context.Context, input SenderRequestInput) (*SenderRequest, error) {
|
||||||
|
req := sender.Request{
|
||||||
|
URL: input.URL,
|
||||||
|
Header: make(http.Header),
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.ID != nil {
|
||||||
|
req.ID = *input.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.Method != nil {
|
||||||
|
req.Method = input.Method.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.Proto != nil {
|
||||||
|
req.Proto = revHTTPProtocolMap[*input.Proto]
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, header := range input.Headers {
|
||||||
|
req.Header.Add(header.Key, header.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.Body != nil {
|
||||||
|
req.Body = []byte(*input.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := r.SenderService.CreateOrUpdateRequest(ctx, req)
|
||||||
|
if errors.Is(err, proj.ErrNoProject) {
|
||||||
|
return nil, noActiveProjectErr(ctx)
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not create sender request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
senderReq, err := parseSenderRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &senderReq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) CreateSenderRequestFromHTTPRequestLog(ctx context.Context, id ulid.ULID) (*SenderRequest, error) {
|
||||||
|
req, err := r.SenderService.CloneFromRequestLog(ctx, id)
|
||||||
|
if errors.Is(err, proj.ErrNoProject) {
|
||||||
|
return nil, noActiveProjectErr(ctx)
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not create sender request from http request log: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
senderReq, err := parseSenderRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &senderReq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) SendRequest(ctx context.Context, id ulid.ULID) (*SenderRequest, error) {
|
||||||
|
// Use new context, because we don't want to risk interrupting sending the request
|
||||||
|
// or the subsequent storing of the response, e.g. if ctx gets cancelled or
|
||||||
|
// times out.
|
||||||
|
ctx2 := context.Background()
|
||||||
|
|
||||||
|
var sendErr *sender.SendError
|
||||||
|
|
||||||
|
req, err := r.SenderService.SendRequest(ctx2, id)
|
||||||
|
if errors.Is(err, proj.ErrNoProject) {
|
||||||
|
return nil, noActiveProjectErr(ctx)
|
||||||
|
} else if errors.As(err, &sendErr) {
|
||||||
|
return nil, &gqlerror.Error{
|
||||||
|
Path: graphql.GetPath(ctx),
|
||||||
|
Message: fmt.Sprintf("Sending request failed: %v", sendErr.Unwrap()),
|
||||||
|
Extensions: map[string]interface{}{
|
||||||
|
"code": "send_request_failed",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not send request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
senderReq, err := parseSenderRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &senderReq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) DeleteSenderRequests(ctx context.Context) (*DeleteSenderRequestsResult, error) {
|
||||||
|
project, err := r.ProjectService.ActiveProject(ctx)
|
||||||
|
if errors.Is(err, proj.ErrNoProject) {
|
||||||
|
return nil, noActiveProjectErr(ctx)
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not get active project: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.SenderService.DeleteRequests(ctx, project.ID); err != nil {
|
||||||
|
return nil, fmt.Errorf("could not clear request log: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DeleteSenderRequestsResult{true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSenderRequest(req sender.Request) (SenderRequest, error) {
|
||||||
|
method := HTTPMethod(req.Method)
|
||||||
|
if method != "" && !method.IsValid() {
|
||||||
|
return SenderRequest{}, fmt.Errorf("sender request has invalid method: %v", method)
|
||||||
|
}
|
||||||
|
|
||||||
|
reqProto := httpProtocolMap[req.Proto]
|
||||||
|
if !reqProto.IsValid() {
|
||||||
|
return SenderRequest{}, fmt.Errorf("sender request has invalid protocol: %v", req.Proto)
|
||||||
|
}
|
||||||
|
|
||||||
|
senderReq := SenderRequest{
|
||||||
|
ID: req.ID,
|
||||||
|
URL: req.URL,
|
||||||
|
Method: method,
|
||||||
|
Proto: HTTPProtocol(req.Proto),
|
||||||
|
Timestamp: ulid.Time(req.ID.Time()),
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.SourceRequestLogID.Compare(ulid.ULID{}) != 0 {
|
||||||
|
senderReq.SourceRequestLogID = &req.SourceRequestLogID
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Header != nil {
|
||||||
|
senderReq.Headers = make([]HTTPHeader, 0)
|
||||||
|
|
||||||
|
for key, values := range req.Header {
|
||||||
|
for _, value := range values {
|
||||||
|
senderReq.Headers = append(senderReq.Headers, HTTPHeader{
|
||||||
|
Key: key,
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Body) > 0 {
|
||||||
|
bodyStr := string(req.Body)
|
||||||
|
senderReq.Body = &bodyStr
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Response != nil {
|
||||||
|
resLog, err := parseResponseLog(*req.Response)
|
||||||
|
if err != nil {
|
||||||
|
return SenderRequest{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resLog.ID = req.ID
|
||||||
|
|
||||||
|
senderReq.Response = &resLog
|
||||||
|
}
|
||||||
|
|
||||||
|
return senderReq, nil
|
||||||
|
}
|
||||||
|
|
||||||
func stringPtrToRegexp(s *string) (*regexp.Regexp, error) {
|
func stringPtrToRegexp(s *string) (*regexp.Regexp, error) {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -364,6 +610,27 @@ func findRequestsFilterFromInput(input *HTTPRequestLogFilterInput) (filter reqlo
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func findSenderRequestsFilterFromInput(input *SenderRequestFilterInput) (filter sender.FindRequestsFilter, err error) {
|
||||||
|
if input == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.OnlyInScope != nil {
|
||||||
|
filter.OnlyInScope = *input.OnlyInScope
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.SearchExpression != nil && *input.SearchExpression != "" {
|
||||||
|
expr, err := search.ParseQuery(*input.SearchExpression)
|
||||||
|
if err != nil {
|
||||||
|
return sender.FindRequestsFilter{}, fmt.Errorf("could not parse search query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.SearchExpr = expr
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func findReqFilterToHTTPReqLogFilter(findReqFilter reqlog.FindRequestsFilter) *HTTPRequestLogFilter {
|
func findReqFilterToHTTPReqLogFilter(findReqFilter reqlog.FindRequestsFilter) *HTTPRequestLogFilter {
|
||||||
empty := reqlog.FindRequestsFilter{}
|
empty := reqlog.FindRequestsFilter{}
|
||||||
if findReqFilter == empty {
|
if findReqFilter == empty {
|
||||||
|
@ -382,6 +649,24 @@ func findReqFilterToHTTPReqLogFilter(findReqFilter reqlog.FindRequestsFilter) *H
|
||||||
return httpReqLogFilter
|
return httpReqLogFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func findReqFilterToSenderReqFilter(findReqFilter sender.FindRequestsFilter) *SenderRequestFilter {
|
||||||
|
empty := sender.FindRequestsFilter{}
|
||||||
|
if findReqFilter == empty {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
senderReqFilter := &SenderRequestFilter{
|
||||||
|
OnlyInScope: findReqFilter.OnlyInScope,
|
||||||
|
}
|
||||||
|
|
||||||
|
if findReqFilter.SearchExpr != nil {
|
||||||
|
searchExpr := findReqFilter.SearchExpr.String()
|
||||||
|
senderReqFilter.SearchExpression = &searchExpr
|
||||||
|
}
|
||||||
|
|
||||||
|
return senderReqFilter
|
||||||
|
}
|
||||||
|
|
||||||
func noActiveProjectErr(ctx context.Context) error {
|
func noActiveProjectErr(ctx context.Context) error {
|
||||||
return &gqlerror.Error{
|
return &gqlerror.Error{
|
||||||
Path: graphql.GetPath(ctx),
|
Path: graphql.GetPath(ctx),
|
||||||
|
|
|
@ -10,7 +10,11 @@ type HttpRequestLog {
|
||||||
}
|
}
|
||||||
|
|
||||||
type HttpResponseLog {
|
type HttpResponseLog {
|
||||||
proto: String!
|
"""
|
||||||
|
Will be the same ID as its related request ID.
|
||||||
|
"""
|
||||||
|
id: ID!
|
||||||
|
proto: HttpProtocol!
|
||||||
statusCode: Int!
|
statusCode: Int!
|
||||||
statusReason: String!
|
statusReason: String!
|
||||||
body: String
|
body: String
|
||||||
|
@ -62,6 +66,10 @@ type ClearHTTPRequestLogResult {
|
||||||
success: Boolean!
|
success: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeleteSenderRequestsResult {
|
||||||
|
success: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
input HttpRequestLogFilterInput {
|
input HttpRequestLogFilterInput {
|
||||||
onlyInScope: Boolean
|
onlyInScope: Boolean
|
||||||
searchExpression: String
|
searchExpression: String
|
||||||
|
@ -72,6 +80,42 @@ type HttpRequestLogFilter {
|
||||||
searchExpression: String
|
searchExpression: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input SenderRequestInput {
|
||||||
|
id: ID
|
||||||
|
url: URL!
|
||||||
|
method: HttpMethod
|
||||||
|
proto: HttpProtocol
|
||||||
|
headers: [HttpHeaderInput!]
|
||||||
|
body: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input HttpHeaderInput {
|
||||||
|
key: String!
|
||||||
|
value: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type SenderRequest {
|
||||||
|
id: ID!
|
||||||
|
sourceRequestLogID: ID
|
||||||
|
url: URL!
|
||||||
|
method: HttpMethod!
|
||||||
|
proto: HttpProtocol!
|
||||||
|
headers: [HttpHeader!]
|
||||||
|
body: String
|
||||||
|
timestamp: Time!
|
||||||
|
response: HttpResponseLog
|
||||||
|
}
|
||||||
|
|
||||||
|
input SenderRequestFilterInput {
|
||||||
|
onlyInScope: Boolean
|
||||||
|
searchExpression: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type SenderRequestFilter {
|
||||||
|
onlyInScope: Boolean!
|
||||||
|
searchExpression: String
|
||||||
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
httpRequestLog(id: ID!): HttpRequestLog
|
httpRequestLog(id: ID!): HttpRequestLog
|
||||||
httpRequestLogs: [HttpRequestLog!]!
|
httpRequestLogs: [HttpRequestLog!]!
|
||||||
|
@ -79,6 +123,8 @@ type Query {
|
||||||
activeProject: Project
|
activeProject: Project
|
||||||
projects: [Project!]!
|
projects: [Project!]!
|
||||||
scope: [ScopeRule!]!
|
scope: [ScopeRule!]!
|
||||||
|
senderRequest(id: ID!): SenderRequest
|
||||||
|
senderRequests: [SenderRequest!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
|
@ -91,6 +137,11 @@ type Mutation {
|
||||||
setHttpRequestLogFilter(
|
setHttpRequestLogFilter(
|
||||||
filter: HttpRequestLogFilterInput
|
filter: HttpRequestLogFilterInput
|
||||||
): HttpRequestLogFilter
|
): HttpRequestLogFilter
|
||||||
|
setSenderRequestFilter(filter: SenderRequestFilterInput): SenderRequestFilter
|
||||||
|
createOrUpdateSenderRequest(request: SenderRequestInput!): SenderRequest!
|
||||||
|
createSenderRequestFromHttpRequestLog(id: ID!): SenderRequest!
|
||||||
|
sendRequest(id: ID!): SenderRequest!
|
||||||
|
deleteSenderRequests: DeleteSenderRequestsResult!
|
||||||
}
|
}
|
||||||
|
|
||||||
enum HttpMethod {
|
enum HttpMethod {
|
||||||
|
@ -105,5 +156,11 @@ enum HttpMethod {
|
||||||
PATCH
|
PATCH
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum HttpProtocol {
|
||||||
|
HTTP1
|
||||||
|
HTTP2
|
||||||
|
}
|
||||||
|
|
||||||
scalar Time
|
scalar Time
|
||||||
scalar Regexp
|
scalar Regexp
|
||||||
|
scalar URL
|
||||||
|
|
|
@ -11,9 +11,13 @@ const (
|
||||||
projectPrefix = 0x00
|
projectPrefix = 0x00
|
||||||
reqLogPrefix = 0x01
|
reqLogPrefix = 0x01
|
||||||
resLogPrefix = 0x02
|
resLogPrefix = 0x02
|
||||||
|
senderReqPrefix = 0x03
|
||||||
|
|
||||||
// Request log indices.
|
// Request log indices.
|
||||||
reqLogProjectIDIndex = 0x00
|
reqLogProjectIDIndex = 0x00
|
||||||
|
|
||||||
|
// Sender request indices.
|
||||||
|
senderReqProjectIDIndex = 0x00
|
||||||
)
|
)
|
||||||
|
|
||||||
// Database is used to store and retrieve data from an underlying Badger database.
|
// Database is used to store and retrieve data from an underlying Badger database.
|
||||||
|
|
|
@ -65,6 +65,11 @@ func (db *Database) DeleteProject(ctx context.Context, projectID ulid.ULID) erro
|
||||||
return fmt.Errorf("badger: failed to delete project request logs: %w", err)
|
return fmt.Errorf("badger: failed to delete project request logs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = db.DeleteSenderRequests(ctx, projectID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("badger: failed to delete project sender requests: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
err = db.badger.Update(func(txn *badger.Txn) error {
|
err = db.badger.Update(func(txn *badger.Txn) error {
|
||||||
return txn.Delete(entryKey(projectPrefix, 0, projectID[:]))
|
return txn.Delete(entryKey(projectPrefix, 0, projectID[:]))
|
||||||
})
|
})
|
||||||
|
|
|
@ -56,6 +56,7 @@ func TestUpsertProject(t *testing.T) {
|
||||||
Settings: proj.Settings{
|
Settings: proj.Settings{
|
||||||
ReqLogBypassOutOfScope: true,
|
ReqLogBypassOutOfScope: true,
|
||||||
ReqLogOnlyFindInScope: true,
|
ReqLogOnlyFindInScope: true,
|
||||||
|
ReqLogSearchExpr: searchExpr,
|
||||||
ScopeRules: []scope.Rule{
|
ScopeRules: []scope.Rule{
|
||||||
{
|
{
|
||||||
URL: regexp.MustCompile("^https://(.*)example.com(.*)$"),
|
URL: regexp.MustCompile("^https://(.*)example.com(.*)$"),
|
||||||
|
@ -66,7 +67,6 @@ func TestUpsertProject(t *testing.T) {
|
||||||
Body: regexp.MustCompile("^foo(.*)"),
|
Body: regexp.MustCompile("^foo(.*)"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
SearchExpr: searchExpr,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,21 +178,38 @@ func TestDeleteProject(t *testing.T) {
|
||||||
// Store fixtures.
|
// Store fixtures.
|
||||||
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
||||||
reqLogID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
reqLogID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
||||||
|
senderReqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
||||||
|
|
||||||
err = badgerDB.Update(func(txn *badgerdb.Txn) error {
|
err = badgerDB.Update(func(txn *badgerdb.Txn) error {
|
||||||
|
// Project item.
|
||||||
if err := txn.Set(entryKey(projectPrefix, 0, projectID[:]), nil); err != nil {
|
if err := txn.Set(entryKey(projectPrefix, 0, projectID[:]), nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sender request items.
|
||||||
|
if err := txn.Set(entryKey(senderReqPrefix, 0, senderReqID[:]), nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := txn.Set(entryKey(resLogPrefix, 0, senderReqID[:]), nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err := txn.Set(entryKey(senderReqPrefix, senderReqProjectIDIndex, append(projectID[:], senderReqID[:]...)), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request log items.
|
||||||
if err := txn.Set(entryKey(reqLogPrefix, 0, reqLogID[:]), nil); err != nil {
|
if err := txn.Set(entryKey(reqLogPrefix, 0, reqLogID[:]), nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := txn.Set(entryKey(resLogPrefix, 0, reqLogID[:]), nil); err != nil {
|
if err := txn.Set(entryKey(resLogPrefix, 0, reqLogID[:]), nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err := txn.Set(entryKey(reqLogPrefix, reqLogProjectIDIndex, append(projectID[:], reqLogID[:]...)), nil)
|
err = txn.Set(entryKey(reqLogPrefix, reqLogProjectIDIndex, append(projectID[:], reqLogID[:]...)), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -222,7 +239,7 @@ func TestDeleteProject(t *testing.T) {
|
||||||
t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err)
|
t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assert response log item was deleted.
|
// Assert response log item related to request log was deleted.
|
||||||
err = badgerDB.View(func(txn *badgerdb.Txn) error {
|
err = badgerDB.View(func(txn *badgerdb.Txn) error {
|
||||||
_, err := txn.Get(entryKey(resLogPrefix, 0, reqLogID[:]))
|
_, err := txn.Get(entryKey(resLogPrefix, 0, reqLogID[:]))
|
||||||
return err
|
return err
|
||||||
|
@ -239,6 +256,33 @@ func TestDeleteProject(t *testing.T) {
|
||||||
if !errors.Is(err, badgerdb.ErrKeyNotFound) {
|
if !errors.Is(err, badgerdb.ErrKeyNotFound) {
|
||||||
t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err)
|
t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assert sender request item was deleted.
|
||||||
|
err = badgerDB.View(func(txn *badgerdb.Txn) error {
|
||||||
|
_, err := txn.Get(entryKey(senderReqPrefix, 0, senderReqID[:]))
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if !errors.Is(err, badgerdb.ErrKeyNotFound) {
|
||||||
|
t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert response log item related to sender request was deleted.
|
||||||
|
err = badgerDB.View(func(txn *badgerdb.Txn) error {
|
||||||
|
_, err := txn.Get(entryKey(resLogPrefix, 0, senderReqID[:]))
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if !errors.Is(err, badgerdb.ErrKeyNotFound) {
|
||||||
|
t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert sender request project ID index key was deleted.
|
||||||
|
err = badgerDB.View(func(txn *badgerdb.Txn) error {
|
||||||
|
_, err := txn.Get(entryKey(senderReqPrefix, senderReqProjectIDIndex, append(projectID[:], senderReqID[:]...)))
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if !errors.Is(err, badgerdb.ErrKeyNotFound) {
|
||||||
|
t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProjects(t *testing.T) {
|
func TestProjects(t *testing.T) {
|
||||||
|
|
|
@ -66,7 +66,11 @@ func (db *Database) FindRequestLogs(ctx context.Context, filter reqlog.FindReque
|
||||||
|
|
||||||
func getRequestLogWithResponse(txn *badger.Txn, reqLogID ulid.ULID) (reqlog.RequestLog, error) {
|
func getRequestLogWithResponse(txn *badger.Txn, reqLogID ulid.ULID) (reqlog.RequestLog, error) {
|
||||||
item, err := txn.Get(entryKey(reqLogPrefix, 0, reqLogID[:]))
|
item, err := txn.Get(entryKey(reqLogPrefix, 0, reqLogID[:]))
|
||||||
if err != nil {
|
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, badger.ErrKeyNotFound):
|
||||||
|
return reqlog.RequestLog{}, reqlog.ErrRequestNotFound
|
||||||
|
case err != nil:
|
||||||
return reqlog.RequestLog{}, fmt.Errorf("failed to lookup request log item: %w", err)
|
return reqlog.RequestLog{}, fmt.Errorf("failed to lookup request log item: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
236
pkg/db/badger/sender.go
Normal file
236
pkg/db/badger/sender.go
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
package badger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/dgraph-io/badger/v3"
|
||||||
|
"github.com/oklog/ulid"
|
||||||
|
|
||||||
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
|
"github.com/dstotijn/hetty/pkg/sender"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (db *Database) StoreSenderRequest(ctx context.Context, req sender.Request) error {
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
|
||||||
|
err := gob.NewEncoder(&buf).Encode(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("badger: failed to encode sender request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := []*badger.Entry{
|
||||||
|
// Sender request itself.
|
||||||
|
{
|
||||||
|
Key: entryKey(senderReqPrefix, 0, req.ID[:]),
|
||||||
|
Value: buf.Bytes(),
|
||||||
|
},
|
||||||
|
// Index by project ID.
|
||||||
|
{
|
||||||
|
Key: entryKey(senderReqPrefix, senderReqProjectIDIndex, append(req.ProjectID[:], req.ID[:]...)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.badger.Update(func(txn *badger.Txn) error {
|
||||||
|
for i := range entries {
|
||||||
|
err := txn.SetEntry(entries[i])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("badger: failed to commit transaction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) FindSenderRequestByID(ctx context.Context, senderReqID ulid.ULID) (sender.Request, error) {
|
||||||
|
txn := db.badger.NewTransaction(false)
|
||||||
|
defer txn.Discard()
|
||||||
|
|
||||||
|
req, err := getSenderRequestWithResponseLog(txn, senderReqID)
|
||||||
|
if err != nil {
|
||||||
|
return sender.Request{}, fmt.Errorf("badger: failed to get sender request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) FindSenderRequests(ctx context.Context, filter sender.FindRequestsFilter, scope *scope.Scope) ([]sender.Request, error) {
|
||||||
|
if filter.ProjectID.Compare(ulid.ULID{}) == 0 {
|
||||||
|
return nil, sender.ErrProjectIDMustBeSet
|
||||||
|
}
|
||||||
|
|
||||||
|
txn := db.badger.NewTransaction(false)
|
||||||
|
defer txn.Discard()
|
||||||
|
|
||||||
|
senderReqIDs, err := findSenderRequestIDsByProjectID(txn, filter.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("badger: failed to find sender request IDs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
senderReqs := make([]sender.Request, 0, len(senderReqIDs))
|
||||||
|
|
||||||
|
for _, id := range senderReqIDs {
|
||||||
|
senderReq, err := getSenderRequestWithResponseLog(txn, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("badger: failed to get sender request (id: %v): %w", id.String(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.OnlyInScope {
|
||||||
|
if !senderReq.MatchScope(scope) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by search expression.
|
||||||
|
// TODO: Once pagination is introduced, this filter logic should be done
|
||||||
|
// as items are retrieved (e.g. when using a `badger.Iterator`).
|
||||||
|
if filter.SearchExpr != nil {
|
||||||
|
match, err := senderReq.Matches(filter.SearchExpr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"badger: failed to match search expression for sender request (id: %v): %w",
|
||||||
|
id.String(), err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !match {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
senderReqs = append(senderReqs, senderReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
return senderReqs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) DeleteSenderRequests(ctx context.Context, projectID ulid.ULID) error {
|
||||||
|
// Note: this transaction is used just for reading; we use the `badger.WriteBatch`
|
||||||
|
// API to bulk delete items.
|
||||||
|
txn := db.badger.NewTransaction(false)
|
||||||
|
defer txn.Discard()
|
||||||
|
|
||||||
|
senderReqIDs, err := findSenderRequestIDsByProjectID(txn, projectID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("badger: failed to find sender request IDs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
writeBatch := db.badger.NewWriteBatch()
|
||||||
|
defer writeBatch.Cancel()
|
||||||
|
|
||||||
|
for _, senderReqID := range senderReqIDs {
|
||||||
|
// Delete sender requests.
|
||||||
|
err := writeBatch.Delete(entryKey(senderReqPrefix, 0, senderReqID[:]))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("badger: failed to delete sender requests: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete related response log.
|
||||||
|
err = writeBatch.Delete(entryKey(resLogPrefix, 0, senderReqID[:]))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("badger: failed to delete request log: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeBatch.Flush(); err != nil {
|
||||||
|
return fmt.Errorf("badger: failed to commit batch write: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.badger.DropPrefix(entryKey(senderReqPrefix, senderReqProjectIDIndex, projectID[:]))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("badger: failed to drop sender request project ID index items: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSenderRequestWithResponseLog(txn *badger.Txn, senderReqID ulid.ULID) (sender.Request, error) {
|
||||||
|
item, err := txn.Get(entryKey(senderReqPrefix, 0, senderReqID[:]))
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, badger.ErrKeyNotFound):
|
||||||
|
return sender.Request{}, sender.ErrRequestNotFound
|
||||||
|
case err != nil:
|
||||||
|
return sender.Request{}, fmt.Errorf("failed to lookup sender request item: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := sender.Request{
|
||||||
|
ID: senderReqID,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = item.Value(func(rawSenderReq []byte) error {
|
||||||
|
err = gob.NewDecoder(bytes.NewReader(rawSenderReq)).Decode(&req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decode sender request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return sender.Request{}, fmt.Errorf("failed to retrieve or parse sender request value: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
item, err = txn.Get(entryKey(resLogPrefix, 0, senderReqID[:]))
|
||||||
|
|
||||||
|
if errors.Is(err, badger.ErrKeyNotFound) {
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return sender.Request{}, fmt.Errorf("failed to get response log: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = item.Value(func(rawReslog []byte) error {
|
||||||
|
var resLog reqlog.ResponseLog
|
||||||
|
err = gob.NewDecoder(bytes.NewReader(rawReslog)).Decode(&resLog)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decode response log: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Response = &resLog
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return sender.Request{}, fmt.Errorf("failed to retrieve or parse response log value: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findSenderRequestIDsByProjectID(txn *badger.Txn, projectID ulid.ULID) ([]ulid.ULID, error) {
|
||||||
|
senderReqIDs := make([]ulid.ULID, 0)
|
||||||
|
opts := badger.DefaultIteratorOptions
|
||||||
|
opts.PrefetchValues = false
|
||||||
|
opts.Reverse = true
|
||||||
|
iterator := txn.NewIterator(opts)
|
||||||
|
defer iterator.Close()
|
||||||
|
|
||||||
|
var projectIndexKey []byte
|
||||||
|
|
||||||
|
prefix := entryKey(senderReqPrefix, senderReqProjectIDIndex, projectID[:])
|
||||||
|
|
||||||
|
for iterator.Seek(append(prefix, 255)); iterator.ValidForPrefix(prefix); iterator.Next() {
|
||||||
|
projectIndexKey = iterator.Item().KeyCopy(projectIndexKey)
|
||||||
|
|
||||||
|
var id ulid.ULID
|
||||||
|
// The request log ID starts *after* the first 2 prefix and index bytes
|
||||||
|
// and the 16 byte project ID.
|
||||||
|
if err := id.UnmarshalBinary(projectIndexKey[18:]); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse sender request ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
senderReqIDs = append(senderReqIDs, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return senderReqIDs, nil
|
||||||
|
}
|
204
pkg/db/badger/sender_test.go
Normal file
204
pkg/db/badger/sender_test.go
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
package badger_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
badgerdb "github.com/dgraph-io/badger/v3"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/oklog/ulid"
|
||||||
|
|
||||||
|
"github.com/dstotijn/hetty/pkg/db/badger"
|
||||||
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
|
"github.com/dstotijn/hetty/pkg/sender"
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint:gosec
|
||||||
|
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
|
var exampleURL = func() *url.URL {
|
||||||
|
u, err := url.Parse("https://example.com/foobar")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return u
|
||||||
|
}()
|
||||||
|
|
||||||
|
func TestFindRequestByID(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
database, err := badger.OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to open badger database: %v", err)
|
||||||
|
}
|
||||||
|
defer database.Close()
|
||||||
|
|
||||||
|
// See: https://go.dev/blog/subtests#cleaning-up-after-a-group-of-parallel-tests
|
||||||
|
t.Run("group", func(t *testing.T) {
|
||||||
|
t.Run("sender request not found", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := database.FindSenderRequestByID(context.Background(), ulid.ULID{})
|
||||||
|
if !errors.Is(err, sender.ErrRequestNotFound) {
|
||||||
|
t.Fatalf("expected `sender.ErrRequestNotFound`, got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("sender request found", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
exp := sender.Request{
|
||||||
|
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
|
||||||
|
ProjectID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
|
||||||
|
SourceRequestLogID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
|
||||||
|
|
||||||
|
URL: exampleURL,
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Proto: sender.HTTPProto2,
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Foo": []string{"bar"},
|
||||||
|
},
|
||||||
|
Body: []byte("foo"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := database.StoreSenderRequest(context.Background(), exp)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error (expected: nil, got: %v)", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resLog := reqlog.ResponseLog{
|
||||||
|
Proto: "HTTP/2.0",
|
||||||
|
Status: "200 OK",
|
||||||
|
StatusCode: 200,
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Yolo": []string{"swag"},
|
||||||
|
},
|
||||||
|
Body: []byte("bar"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = database.StoreResponseLog(context.Background(), exp.ID, resLog)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error (expected: nil, got: %v)", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exp.Response = &resLog
|
||||||
|
|
||||||
|
got, err := database.FindSenderRequestByID(context.Background(), exp.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error (expected: nil, got: %v)", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(exp, got); diff != "" {
|
||||||
|
t.Fatalf("sender request not equal (-exp, +got):\n%v", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindSenderRequests(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("without project ID in filter", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
database, err := badger.OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to open badger database: %v", err)
|
||||||
|
}
|
||||||
|
defer database.Close()
|
||||||
|
|
||||||
|
filter := sender.FindRequestsFilter{}
|
||||||
|
|
||||||
|
_, err = database.FindSenderRequests(context.Background(), filter, nil)
|
||||||
|
if !errors.Is(err, sender.ErrProjectIDMustBeSet) {
|
||||||
|
t.Fatalf("expected `sender.ErrProjectIDMustBeSet`, got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns sender requests and related response logs", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
database, err := badger.OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to open badger database: %v", err)
|
||||||
|
}
|
||||||
|
defer database.Close()
|
||||||
|
|
||||||
|
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
||||||
|
|
||||||
|
fixtures := []sender.Request{
|
||||||
|
{
|
||||||
|
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
|
||||||
|
ProjectID: projectID,
|
||||||
|
SourceRequestLogID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
|
||||||
|
URL: exampleURL,
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Foo": []string{"baz"},
|
||||||
|
},
|
||||||
|
Body: []byte("foo"),
|
||||||
|
Response: &reqlog.ResponseLog{
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
Status: "200 OK",
|
||||||
|
StatusCode: 200,
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Yolo": []string{"swag"},
|
||||||
|
},
|
||||||
|
Body: []byte("bar"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: ulid.MustNew(ulid.Timestamp(time.Now())+100, ulidEntropy),
|
||||||
|
ProjectID: projectID,
|
||||||
|
SourceRequestLogID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
|
||||||
|
URL: exampleURL,
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Foo": []string{"baz"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store fixtures.
|
||||||
|
for _, senderReq := range fixtures {
|
||||||
|
err = database.StoreSenderRequest(context.Background(), senderReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error creating request log fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if senderReq.Response != nil {
|
||||||
|
err = database.StoreResponseLog(context.Background(), senderReq.ID, *senderReq.Response)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error creating response log fixture: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := sender.FindRequestsFilter{
|
||||||
|
ProjectID: projectID,
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := database.FindSenderRequests(context.Background(), filter, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error finding sender requests: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We expect the found sender requests are *reversed*, e.g. newest first.
|
||||||
|
exp := make([]sender.Request, len(fixtures))
|
||||||
|
for i, j := 0, len(fixtures)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
exp[i], exp[j] = fixtures[j], fixtures[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(exp, got); diff != "" {
|
||||||
|
t.Fatalf("sender requests not equal (-exp, +got):\n%v", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
"github.com/dstotijn/hetty/pkg/scope"
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
"github.com/dstotijn/hetty/pkg/search"
|
"github.com/dstotijn/hetty/pkg/search"
|
||||||
|
"github.com/dstotijn/hetty/pkg/sender"
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:gosec
|
//nolint:gosec
|
||||||
|
@ -37,13 +38,15 @@ type Service interface {
|
||||||
Scope() *scope.Scope
|
Scope() *scope.Scope
|
||||||
SetScopeRules(ctx context.Context, rules []scope.Rule) error
|
SetScopeRules(ctx context.Context, rules []scope.Rule) error
|
||||||
SetRequestLogFindFilter(ctx context.Context, filter reqlog.FindRequestsFilter) error
|
SetRequestLogFindFilter(ctx context.Context, filter reqlog.FindRequestsFilter) error
|
||||||
|
SetSenderRequestFindFilter(ctx context.Context, filter sender.FindRequestsFilter) error
|
||||||
OnProjectOpen(fn OnProjectOpenFn)
|
OnProjectOpen(fn OnProjectOpenFn)
|
||||||
OnProjectClose(fn OnProjectCloseFn)
|
OnProjectClose(fn OnProjectCloseFn)
|
||||||
}
|
}
|
||||||
|
|
||||||
type service struct {
|
type service struct {
|
||||||
repo Repository
|
repo Repository
|
||||||
reqLogSvc *reqlog.Service
|
reqLogSvc reqlog.Service
|
||||||
|
senderSvc sender.Service
|
||||||
scope *scope.Scope
|
scope *scope.Scope
|
||||||
activeProjectID ulid.ULID
|
activeProjectID ulid.ULID
|
||||||
onProjectOpenFns []OnProjectOpenFn
|
onProjectOpenFns []OnProjectOpenFn
|
||||||
|
@ -62,8 +65,12 @@ type Project struct {
|
||||||
type Settings struct {
|
type Settings struct {
|
||||||
ReqLogBypassOutOfScope bool
|
ReqLogBypassOutOfScope bool
|
||||||
ReqLogOnlyFindInScope bool
|
ReqLogOnlyFindInScope bool
|
||||||
|
ReqLogSearchExpr search.Expression
|
||||||
|
|
||||||
|
SenderOnlyFindInScope bool
|
||||||
|
SenderSearchExpr search.Expression
|
||||||
|
|
||||||
ScopeRules []scope.Rule
|
ScopeRules []scope.Rule
|
||||||
SearchExpr search.Expression
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -77,7 +84,8 @@ var nameRegexp = regexp.MustCompile(`^[\w\d\s]+$`)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Repository Repository
|
Repository Repository
|
||||||
ReqLogService *reqlog.Service
|
ReqLogService reqlog.Service
|
||||||
|
SenderService sender.Service
|
||||||
Scope *scope.Scope
|
Scope *scope.Scope
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,6 +94,7 @@ func NewService(cfg Config) (Service, error) {
|
||||||
return &service{
|
return &service{
|
||||||
repo: cfg.Repository,
|
repo: cfg.Repository,
|
||||||
reqLogSvc: cfg.ReqLogService,
|
reqLogSvc: cfg.ReqLogService,
|
||||||
|
senderSvc: cfg.SenderService,
|
||||||
scope: cfg.Scope,
|
scope: cfg.Scope,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -120,9 +129,11 @@ func (svc *service) CloseProject() error {
|
||||||
closedProjectID := svc.activeProjectID
|
closedProjectID := svc.activeProjectID
|
||||||
|
|
||||||
svc.activeProjectID = ulid.ULID{}
|
svc.activeProjectID = ulid.ULID{}
|
||||||
svc.reqLogSvc.ActiveProjectID = ulid.ULID{}
|
svc.reqLogSvc.SetActiveProjectID(ulid.ULID{})
|
||||||
svc.reqLogSvc.BypassOutOfScopeRequests = false
|
svc.reqLogSvc.SetBypassOutOfScopeRequests(false)
|
||||||
svc.reqLogSvc.FindReqsFilter = reqlog.FindRequestsFilter{}
|
svc.reqLogSvc.SetFindReqsFilter(reqlog.FindRequestsFilter{})
|
||||||
|
svc.senderSvc.SetActiveProjectID(ulid.ULID{})
|
||||||
|
svc.senderSvc.SetFindReqsFilter(sender.FindRequestsFilter{})
|
||||||
svc.scope.SetRules(nil)
|
svc.scope.SetRules(nil)
|
||||||
|
|
||||||
svc.emitProjectClosed(closedProjectID)
|
svc.emitProjectClosed(closedProjectID)
|
||||||
|
@ -154,13 +165,21 @@ func (svc *service) OpenProject(ctx context.Context, projectID ulid.ULID) (Proje
|
||||||
}
|
}
|
||||||
|
|
||||||
svc.activeProjectID = project.ID
|
svc.activeProjectID = project.ID
|
||||||
svc.reqLogSvc.FindReqsFilter = reqlog.FindRequestsFilter{
|
|
||||||
|
svc.reqLogSvc.SetFindReqsFilter(reqlog.FindRequestsFilter{
|
||||||
ProjectID: project.ID,
|
ProjectID: project.ID,
|
||||||
OnlyInScope: project.Settings.ReqLogOnlyFindInScope,
|
OnlyInScope: project.Settings.ReqLogOnlyFindInScope,
|
||||||
SearchExpr: project.Settings.SearchExpr,
|
SearchExpr: project.Settings.ReqLogSearchExpr,
|
||||||
}
|
})
|
||||||
svc.reqLogSvc.BypassOutOfScopeRequests = project.Settings.ReqLogBypassOutOfScope
|
svc.reqLogSvc.SetBypassOutOfScopeRequests(project.Settings.ReqLogBypassOutOfScope)
|
||||||
svc.reqLogSvc.ActiveProjectID = project.ID
|
svc.reqLogSvc.SetActiveProjectID(project.ID)
|
||||||
|
|
||||||
|
svc.senderSvc.SetActiveProjectID(project.ID)
|
||||||
|
svc.senderSvc.SetFindReqsFilter(sender.FindRequestsFilter{
|
||||||
|
ProjectID: project.ID,
|
||||||
|
OnlyInScope: project.Settings.SenderOnlyFindInScope,
|
||||||
|
SearchExpr: project.Settings.SenderSearchExpr,
|
||||||
|
})
|
||||||
|
|
||||||
svc.scope.SetRules(project.Settings.ScopeRules)
|
svc.scope.SetRules(project.Settings.ScopeRules)
|
||||||
|
|
||||||
|
@ -255,14 +274,35 @@ func (svc *service) SetRequestLogFindFilter(ctx context.Context, filter reqlog.F
|
||||||
filter.ProjectID = project.ID
|
filter.ProjectID = project.ID
|
||||||
|
|
||||||
project.Settings.ReqLogOnlyFindInScope = filter.OnlyInScope
|
project.Settings.ReqLogOnlyFindInScope = filter.OnlyInScope
|
||||||
project.Settings.SearchExpr = filter.SearchExpr
|
project.Settings.ReqLogSearchExpr = filter.SearchExpr
|
||||||
|
|
||||||
err = svc.repo.UpsertProject(ctx, project)
|
err = svc.repo.UpsertProject(ctx, project)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("proj: failed to update project: %w", err)
|
return fmt.Errorf("proj: failed to update project: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
svc.reqLogSvc.FindReqsFilter = filter
|
svc.reqLogSvc.SetFindReqsFilter(filter)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *service) SetSenderRequestFindFilter(ctx context.Context, filter sender.FindRequestsFilter) error {
|
||||||
|
project, err := svc.ActiveProject(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.ProjectID = project.ID
|
||||||
|
|
||||||
|
project.Settings.SenderOnlyFindInScope = filter.OnlyInScope
|
||||||
|
project.Settings.SenderSearchExpr = filter.SearchExpr
|
||||||
|
|
||||||
|
err = svc.repo.UpsertProject(ctx, project)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("proj: failed to update project: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.senderSvc.SetFindReqsFilter(filter)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,11 +54,24 @@ type ResponseLog struct {
|
||||||
Body []byte
|
Body []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service interface {
|
||||||
BypassOutOfScopeRequests bool
|
FindRequests(ctx context.Context) ([]RequestLog, error)
|
||||||
FindReqsFilter FindRequestsFilter
|
FindRequestLogByID(ctx context.Context, id ulid.ULID) (RequestLog, error)
|
||||||
ActiveProjectID ulid.ULID
|
ClearRequests(ctx context.Context, projectID ulid.ULID) error
|
||||||
|
RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc
|
||||||
|
ResponseModifier(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc
|
||||||
|
SetActiveProjectID(id ulid.ULID)
|
||||||
|
ActiveProjectID() ulid.ULID
|
||||||
|
SetBypassOutOfScopeRequests(bool)
|
||||||
|
BypassOutOfScopeRequests() bool
|
||||||
|
SetFindReqsFilter(filter FindRequestsFilter)
|
||||||
|
FindReqsFilter() FindRequestsFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
bypassOutOfScopeRequests bool
|
||||||
|
findReqsFilter FindRequestsFilter
|
||||||
|
activeProjectID ulid.ULID
|
||||||
scope *scope.Scope
|
scope *scope.Scope
|
||||||
repo Repository
|
repo Repository
|
||||||
}
|
}
|
||||||
|
@ -74,59 +87,35 @@ type Config struct {
|
||||||
Repository Repository
|
Repository Repository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(cfg Config) *Service {
|
func NewService(cfg Config) Service {
|
||||||
return &Service{
|
return &service{
|
||||||
repo: cfg.Repository,
|
repo: cfg.Repository,
|
||||||
scope: cfg.Scope,
|
scope: cfg.Scope,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) FindRequests(ctx context.Context) ([]RequestLog, error) {
|
func (svc *service) FindRequests(ctx context.Context) ([]RequestLog, error) {
|
||||||
return svc.repo.FindRequestLogs(ctx, svc.FindReqsFilter, svc.scope)
|
return svc.repo.FindRequestLogs(ctx, svc.findReqsFilter, svc.scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) FindRequestLogByID(ctx context.Context, id ulid.ULID) (RequestLog, error) {
|
func (svc *service) FindRequestLogByID(ctx context.Context, id ulid.ULID) (RequestLog, error) {
|
||||||
return svc.repo.FindRequestLogByID(ctx, id)
|
return svc.repo.FindRequestLogByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) ClearRequests(ctx context.Context, projectID ulid.ULID) error {
|
func (svc *service) ClearRequests(ctx context.Context, projectID ulid.ULID) error {
|
||||||
return svc.repo.ClearRequestLogs(ctx, projectID)
|
return svc.repo.ClearRequestLogs(ctx, projectID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) storeResponse(ctx context.Context, reqLogID ulid.ULID, res *http.Response) error {
|
func (svc *service) storeResponse(ctx context.Context, reqLogID ulid.ULID, res *http.Response) error {
|
||||||
if res.Header.Get("Content-Encoding") == "gzip" {
|
resLog, err := ParseHTTPResponse(res)
|
||||||
gzipReader, err := gzip.NewReader(res.Body)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not create gzip reader: %w", err)
|
return err
|
||||||
}
|
|
||||||
defer gzipReader.Close()
|
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
|
|
||||||
if _, err := io.Copy(buf, gzipReader); err != nil {
|
|
||||||
return fmt.Errorf("could not read gzipped response body: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Body = io.NopCloser(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not read body: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resLog := ResponseLog{
|
|
||||||
Proto: res.Proto,
|
|
||||||
StatusCode: res.StatusCode,
|
|
||||||
Status: res.Status,
|
|
||||||
Header: res.Header,
|
|
||||||
Body: body,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return svc.repo.StoreResponseLog(ctx, reqLogID, resLog)
|
return svc.repo.StoreResponseLog(ctx, reqLogID, resLog)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc {
|
func (svc *service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc {
|
||||||
return func(req *http.Request) {
|
return func(req *http.Request) {
|
||||||
next(req)
|
next(req)
|
||||||
|
|
||||||
|
@ -149,7 +138,7 @@ func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bypass logging if no project is active.
|
// Bypass logging if no project is active.
|
||||||
if svc.ActiveProjectID.Compare(ulid.ULID{}) == 0 {
|
if svc.activeProjectID.Compare(ulid.ULID{}) == 0 {
|
||||||
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
|
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
|
||||||
*req = *req.WithContext(ctx)
|
*req = *req.WithContext(ctx)
|
||||||
|
|
||||||
|
@ -158,7 +147,7 @@ func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
|
||||||
|
|
||||||
// Bypass logging if this setting is enabled and the incoming request
|
// Bypass logging if this setting is enabled and the incoming request
|
||||||
// doesn't match any scope rules.
|
// doesn't match any scope rules.
|
||||||
if svc.BypassOutOfScopeRequests && !svc.scope.Match(clone, body) {
|
if svc.bypassOutOfScopeRequests && !svc.scope.Match(clone, body) {
|
||||||
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
|
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
|
||||||
*req = *req.WithContext(ctx)
|
*req = *req.WithContext(ctx)
|
||||||
|
|
||||||
|
@ -167,7 +156,7 @@ func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
|
||||||
|
|
||||||
reqLog := RequestLog{
|
reqLog := RequestLog{
|
||||||
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
|
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
|
||||||
ProjectID: svc.ActiveProjectID,
|
ProjectID: svc.activeProjectID,
|
||||||
Method: clone.Method,
|
Method: clone.Method,
|
||||||
URL: clone.URL,
|
URL: clone.URL,
|
||||||
Proto: clone.Proto,
|
Proto: clone.Proto,
|
||||||
|
@ -186,7 +175,7 @@ func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc {
|
func (svc *service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc {
|
||||||
return func(res *http.Response) error {
|
return func(res *http.Response) error {
|
||||||
if err := next(res); err != nil {
|
if err := next(res); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -221,3 +210,58 @@ func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.Respon
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (svc *service) SetActiveProjectID(id ulid.ULID) {
|
||||||
|
svc.activeProjectID = id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *service) ActiveProjectID() ulid.ULID {
|
||||||
|
return svc.activeProjectID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *service) SetFindReqsFilter(filter FindRequestsFilter) {
|
||||||
|
svc.findReqsFilter = filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *service) FindReqsFilter() FindRequestsFilter {
|
||||||
|
return svc.findReqsFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *service) SetBypassOutOfScopeRequests(bypass bool) {
|
||||||
|
svc.bypassOutOfScopeRequests = bypass
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *service) BypassOutOfScopeRequests() bool {
|
||||||
|
return svc.bypassOutOfScopeRequests
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseHTTPResponse(res *http.Response) (ResponseLog, error) {
|
||||||
|
if res.Header.Get("Content-Encoding") == "gzip" {
|
||||||
|
gzipReader, err := gzip.NewReader(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return ResponseLog{}, fmt.Errorf("reqlog: could not create gzip reader: %w", err)
|
||||||
|
}
|
||||||
|
defer gzipReader.Close()
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
|
||||||
|
if _, err := io.Copy(buf, gzipReader); err != nil {
|
||||||
|
return ResponseLog{}, fmt.Errorf("reqlog: could not read gzipped response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Body = io.NopCloser(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return ResponseLog{}, fmt.Errorf("reqlog: could not read body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseLog{
|
||||||
|
Proto: res.Proto,
|
||||||
|
StatusCode: res.StatusCode,
|
||||||
|
Status: res.Status,
|
||||||
|
Header: res.Header,
|
||||||
|
Body: body,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ func TestRequestModifier(t *testing.T) {
|
||||||
Repository: repoMock,
|
Repository: repoMock,
|
||||||
Scope: &scope.Scope{},
|
Scope: &scope.Scope{},
|
||||||
})
|
})
|
||||||
svc.ActiveProjectID = ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
svc.SetActiveProjectID(ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy))
|
||||||
|
|
||||||
next := func(req *http.Request) {
|
next := func(req *http.Request) {
|
||||||
req.Body = io.NopCloser(strings.NewReader("modified body"))
|
req.Body = io.NopCloser(strings.NewReader("modified body"))
|
||||||
|
@ -52,7 +52,7 @@ func TestRequestModifier(t *testing.T) {
|
||||||
|
|
||||||
exp := reqlog.RequestLog{
|
exp := reqlog.RequestLog{
|
||||||
ID: ulid.ULID{}, // Empty value
|
ID: ulid.ULID{}, // Empty value
|
||||||
ProjectID: svc.ActiveProjectID,
|
ProjectID: svc.ActiveProjectID(),
|
||||||
Method: req.Method,
|
Method: req.Method,
|
||||||
URL: req.URL,
|
URL: req.URL,
|
||||||
Proto: req.Proto,
|
Proto: req.Proto,
|
||||||
|
@ -78,7 +78,7 @@ func TestResponseModifier(t *testing.T) {
|
||||||
svc := reqlog.NewService(reqlog.Config{
|
svc := reqlog.NewService(reqlog.Config{
|
||||||
Repository: repoMock,
|
Repository: repoMock,
|
||||||
})
|
})
|
||||||
svc.ActiveProjectID = ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
svc.SetActiveProjectID(ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy))
|
||||||
|
|
||||||
next := func(res *http.Response) error {
|
next := func(res *http.Response) error {
|
||||||
res.Body = io.NopCloser(strings.NewReader("modified body"))
|
res.Body = io.NopCloser(strings.NewReader("modified body"))
|
||||||
|
|
|
@ -27,7 +27,7 @@ var reqLogSearchKeyFns = map[string]func(rl RequestLog) string{
|
||||||
"req.timestamp": func(rl RequestLog) string { return ulid.Time(rl.ID.Time()).String() },
|
"req.timestamp": func(rl RequestLog) string { return ulid.Time(rl.ID.Time()).String() },
|
||||||
}
|
}
|
||||||
|
|
||||||
var resLogSearchKeyFns = map[string]func(rl ResponseLog) string{
|
var ResLogSearchKeyFns = map[string]func(rl ResponseLog) string{
|
||||||
"res.proto": func(rl ResponseLog) string { return rl.Proto },
|
"res.proto": func(rl ResponseLog) string { return rl.Proto },
|
||||||
"res.statusCode": func(rl ResponseLog) string { return strconv.Itoa(rl.StatusCode) },
|
"res.statusCode": func(rl ResponseLog) string { return strconv.Itoa(rl.StatusCode) },
|
||||||
"res.statusReason": func(rl ResponseLog) string { return rl.Status },
|
"res.statusReason": func(rl ResponseLog) string { return rl.Status },
|
||||||
|
@ -154,7 +154,7 @@ func (reqLog RequestLog) getMappedStringLiteral(s string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
fn, ok := resLogSearchKeyFns[s]
|
fn, ok := ResLogSearchKeyFns[s]
|
||||||
if ok {
|
if ok {
|
||||||
return fn(*reqLog.Response)
|
return fn(*reqLog.Response)
|
||||||
}
|
}
|
||||||
|
@ -174,7 +174,7 @@ func (reqLog RequestLog) matchStringLiteral(strLiteral search.StringLiteral) (bo
|
||||||
}
|
}
|
||||||
|
|
||||||
if reqLog.Response != nil {
|
if reqLog.Response != nil {
|
||||||
for _, fn := range resLogSearchKeyFns {
|
for _, fn := range ResLogSearchKeyFns {
|
||||||
if strings.Contains(
|
if strings.Contains(
|
||||||
strings.ToLower(fn(*reqLog.Response)),
|
strings.ToLower(fn(*reqLog.Response)),
|
||||||
strings.ToLower(strLiteral.Value),
|
strings.ToLower(strLiteral.Value),
|
||||||
|
|
18
pkg/sender/repo.go
Normal file
18
pkg/sender/repo.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package sender
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid"
|
||||||
|
|
||||||
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository interface {
|
||||||
|
FindSenderRequestByID(ctx context.Context, id ulid.ULID) (Request, error)
|
||||||
|
FindSenderRequests(ctx context.Context, filter FindRequestsFilter, scope *scope.Scope) ([]Request, error)
|
||||||
|
StoreSenderRequest(ctx context.Context, req Request) error
|
||||||
|
StoreResponseLog(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error
|
||||||
|
DeleteSenderRequests(ctx context.Context, projectID ulid.ULID) error
|
||||||
|
}
|
292
pkg/sender/repo_mock_test.go
Normal file
292
pkg/sender/repo_mock_test.go
Normal file
|
@ -0,0 +1,292 @@
|
||||||
|
// Code generated by moq; DO NOT EDIT.
|
||||||
|
// github.com/matryer/moq
|
||||||
|
|
||||||
|
package sender_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
|
"github.com/dstotijn/hetty/pkg/sender"
|
||||||
|
"github.com/oklog/ulid"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure, that RepoMock does implement sender.Repository.
|
||||||
|
// If this is not the case, regenerate this file with moq.
|
||||||
|
var _ sender.Repository = &RepoMock{}
|
||||||
|
|
||||||
|
// RepoMock is a mock implementation of sender.Repository.
|
||||||
|
//
|
||||||
|
// func TestSomethingThatUsesRepository(t *testing.T) {
|
||||||
|
//
|
||||||
|
// // make and configure a mocked sender.Repository
|
||||||
|
// mockedRepository := &RepoMock{
|
||||||
|
// DeleteSenderRequestsFunc: func(ctx context.Context, projectID ulid.ULID) error {
|
||||||
|
// panic("mock out the DeleteSenderRequests method")
|
||||||
|
// },
|
||||||
|
// FindSenderRequestByIDFunc: func(ctx context.Context, id ulid.ULID) (sender.Request, error) {
|
||||||
|
// panic("mock out the FindSenderRequestByID method")
|
||||||
|
// },
|
||||||
|
// FindSenderRequestsFunc: func(ctx context.Context, filter sender.FindRequestsFilter, scopeMoqParam *scope.Scope) ([]sender.Request, error) {
|
||||||
|
// panic("mock out the FindSenderRequests method")
|
||||||
|
// },
|
||||||
|
// StoreResponseLogFunc: func(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error {
|
||||||
|
// panic("mock out the StoreResponseLog method")
|
||||||
|
// },
|
||||||
|
// StoreSenderRequestFunc: func(ctx context.Context, req sender.Request) error {
|
||||||
|
// panic("mock out the StoreSenderRequest method")
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // use mockedRepository in code that requires sender.Repository
|
||||||
|
// // and then make assertions.
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
type RepoMock struct {
|
||||||
|
// DeleteSenderRequestsFunc mocks the DeleteSenderRequests method.
|
||||||
|
DeleteSenderRequestsFunc func(ctx context.Context, projectID ulid.ULID) error
|
||||||
|
|
||||||
|
// FindSenderRequestByIDFunc mocks the FindSenderRequestByID method.
|
||||||
|
FindSenderRequestByIDFunc func(ctx context.Context, id ulid.ULID) (sender.Request, error)
|
||||||
|
|
||||||
|
// FindSenderRequestsFunc mocks the FindSenderRequests method.
|
||||||
|
FindSenderRequestsFunc func(ctx context.Context, filter sender.FindRequestsFilter, scopeMoqParam *scope.Scope) ([]sender.Request, error)
|
||||||
|
|
||||||
|
// StoreResponseLogFunc mocks the StoreResponseLog method.
|
||||||
|
StoreResponseLogFunc func(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error
|
||||||
|
|
||||||
|
// StoreSenderRequestFunc mocks the StoreSenderRequest method.
|
||||||
|
StoreSenderRequestFunc func(ctx context.Context, req sender.Request) error
|
||||||
|
|
||||||
|
// calls tracks calls to the methods.
|
||||||
|
calls struct {
|
||||||
|
// DeleteSenderRequests holds details about calls to the DeleteSenderRequests method.
|
||||||
|
DeleteSenderRequests []struct {
|
||||||
|
// Ctx is the ctx argument value.
|
||||||
|
Ctx context.Context
|
||||||
|
// ProjectID is the projectID argument value.
|
||||||
|
ProjectID ulid.ULID
|
||||||
|
}
|
||||||
|
// FindSenderRequestByID holds details about calls to the FindSenderRequestByID method.
|
||||||
|
FindSenderRequestByID []struct {
|
||||||
|
// Ctx is the ctx argument value.
|
||||||
|
Ctx context.Context
|
||||||
|
// ID is the id argument value.
|
||||||
|
ID ulid.ULID
|
||||||
|
}
|
||||||
|
// FindSenderRequests holds details about calls to the FindSenderRequests method.
|
||||||
|
FindSenderRequests []struct {
|
||||||
|
// Ctx is the ctx argument value.
|
||||||
|
Ctx context.Context
|
||||||
|
// Filter is the filter argument value.
|
||||||
|
Filter sender.FindRequestsFilter
|
||||||
|
// ScopeMoqParam is the scopeMoqParam argument value.
|
||||||
|
ScopeMoqParam *scope.Scope
|
||||||
|
}
|
||||||
|
// StoreResponseLog holds details about calls to the StoreResponseLog method.
|
||||||
|
StoreResponseLog []struct {
|
||||||
|
// Ctx is the ctx argument value.
|
||||||
|
Ctx context.Context
|
||||||
|
// ReqLogID is the reqLogID argument value.
|
||||||
|
ReqLogID ulid.ULID
|
||||||
|
// ResLog is the resLog argument value.
|
||||||
|
ResLog reqlog.ResponseLog
|
||||||
|
}
|
||||||
|
// StoreSenderRequest holds details about calls to the StoreSenderRequest method.
|
||||||
|
StoreSenderRequest []struct {
|
||||||
|
// Ctx is the ctx argument value.
|
||||||
|
Ctx context.Context
|
||||||
|
// Req is the req argument value.
|
||||||
|
Req sender.Request
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lockDeleteSenderRequests sync.RWMutex
|
||||||
|
lockFindSenderRequestByID sync.RWMutex
|
||||||
|
lockFindSenderRequests sync.RWMutex
|
||||||
|
lockStoreResponseLog sync.RWMutex
|
||||||
|
lockStoreSenderRequest sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSenderRequests calls DeleteSenderRequestsFunc.
|
||||||
|
func (mock *RepoMock) DeleteSenderRequests(ctx context.Context, projectID ulid.ULID) error {
|
||||||
|
if mock.DeleteSenderRequestsFunc == nil {
|
||||||
|
panic("RepoMock.DeleteSenderRequestsFunc: method is nil but Repository.DeleteSenderRequests was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Ctx context.Context
|
||||||
|
ProjectID ulid.ULID
|
||||||
|
}{
|
||||||
|
Ctx: ctx,
|
||||||
|
ProjectID: projectID,
|
||||||
|
}
|
||||||
|
mock.lockDeleteSenderRequests.Lock()
|
||||||
|
mock.calls.DeleteSenderRequests = append(mock.calls.DeleteSenderRequests, callInfo)
|
||||||
|
mock.lockDeleteSenderRequests.Unlock()
|
||||||
|
return mock.DeleteSenderRequestsFunc(ctx, projectID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSenderRequestsCalls gets all the calls that were made to DeleteSenderRequests.
|
||||||
|
// Check the length with:
|
||||||
|
// len(mockedRepository.DeleteSenderRequestsCalls())
|
||||||
|
func (mock *RepoMock) DeleteSenderRequestsCalls() []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
ProjectID ulid.ULID
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
ProjectID ulid.ULID
|
||||||
|
}
|
||||||
|
mock.lockDeleteSenderRequests.RLock()
|
||||||
|
calls = mock.calls.DeleteSenderRequests
|
||||||
|
mock.lockDeleteSenderRequests.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindSenderRequestByID calls FindSenderRequestByIDFunc.
|
||||||
|
func (mock *RepoMock) FindSenderRequestByID(ctx context.Context, id ulid.ULID) (sender.Request, error) {
|
||||||
|
if mock.FindSenderRequestByIDFunc == nil {
|
||||||
|
panic("RepoMock.FindSenderRequestByIDFunc: method is nil but Repository.FindSenderRequestByID was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Ctx context.Context
|
||||||
|
ID ulid.ULID
|
||||||
|
}{
|
||||||
|
Ctx: ctx,
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
mock.lockFindSenderRequestByID.Lock()
|
||||||
|
mock.calls.FindSenderRequestByID = append(mock.calls.FindSenderRequestByID, callInfo)
|
||||||
|
mock.lockFindSenderRequestByID.Unlock()
|
||||||
|
return mock.FindSenderRequestByIDFunc(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindSenderRequestByIDCalls gets all the calls that were made to FindSenderRequestByID.
|
||||||
|
// Check the length with:
|
||||||
|
// len(mockedRepository.FindSenderRequestByIDCalls())
|
||||||
|
func (mock *RepoMock) FindSenderRequestByIDCalls() []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
ID ulid.ULID
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
ID ulid.ULID
|
||||||
|
}
|
||||||
|
mock.lockFindSenderRequestByID.RLock()
|
||||||
|
calls = mock.calls.FindSenderRequestByID
|
||||||
|
mock.lockFindSenderRequestByID.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindSenderRequests calls FindSenderRequestsFunc.
|
||||||
|
func (mock *RepoMock) FindSenderRequests(ctx context.Context, filter sender.FindRequestsFilter, scopeMoqParam *scope.Scope) ([]sender.Request, error) {
|
||||||
|
if mock.FindSenderRequestsFunc == nil {
|
||||||
|
panic("RepoMock.FindSenderRequestsFunc: method is nil but Repository.FindSenderRequests was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Ctx context.Context
|
||||||
|
Filter sender.FindRequestsFilter
|
||||||
|
ScopeMoqParam *scope.Scope
|
||||||
|
}{
|
||||||
|
Ctx: ctx,
|
||||||
|
Filter: filter,
|
||||||
|
ScopeMoqParam: scopeMoqParam,
|
||||||
|
}
|
||||||
|
mock.lockFindSenderRequests.Lock()
|
||||||
|
mock.calls.FindSenderRequests = append(mock.calls.FindSenderRequests, callInfo)
|
||||||
|
mock.lockFindSenderRequests.Unlock()
|
||||||
|
return mock.FindSenderRequestsFunc(ctx, filter, scopeMoqParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindSenderRequestsCalls gets all the calls that were made to FindSenderRequests.
|
||||||
|
// Check the length with:
|
||||||
|
// len(mockedRepository.FindSenderRequestsCalls())
|
||||||
|
func (mock *RepoMock) FindSenderRequestsCalls() []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
Filter sender.FindRequestsFilter
|
||||||
|
ScopeMoqParam *scope.Scope
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
Filter sender.FindRequestsFilter
|
||||||
|
ScopeMoqParam *scope.Scope
|
||||||
|
}
|
||||||
|
mock.lockFindSenderRequests.RLock()
|
||||||
|
calls = mock.calls.FindSenderRequests
|
||||||
|
mock.lockFindSenderRequests.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreResponseLog calls StoreResponseLogFunc.
|
||||||
|
func (mock *RepoMock) StoreResponseLog(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error {
|
||||||
|
if mock.StoreResponseLogFunc == nil {
|
||||||
|
panic("RepoMock.StoreResponseLogFunc: method is nil but Repository.StoreResponseLog was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Ctx context.Context
|
||||||
|
ReqLogID ulid.ULID
|
||||||
|
ResLog reqlog.ResponseLog
|
||||||
|
}{
|
||||||
|
Ctx: ctx,
|
||||||
|
ReqLogID: reqLogID,
|
||||||
|
ResLog: resLog,
|
||||||
|
}
|
||||||
|
mock.lockStoreResponseLog.Lock()
|
||||||
|
mock.calls.StoreResponseLog = append(mock.calls.StoreResponseLog, callInfo)
|
||||||
|
mock.lockStoreResponseLog.Unlock()
|
||||||
|
return mock.StoreResponseLogFunc(ctx, reqLogID, resLog)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreResponseLogCalls gets all the calls that were made to StoreResponseLog.
|
||||||
|
// Check the length with:
|
||||||
|
// len(mockedRepository.StoreResponseLogCalls())
|
||||||
|
func (mock *RepoMock) StoreResponseLogCalls() []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
ReqLogID ulid.ULID
|
||||||
|
ResLog reqlog.ResponseLog
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
ReqLogID ulid.ULID
|
||||||
|
ResLog reqlog.ResponseLog
|
||||||
|
}
|
||||||
|
mock.lockStoreResponseLog.RLock()
|
||||||
|
calls = mock.calls.StoreResponseLog
|
||||||
|
mock.lockStoreResponseLog.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreSenderRequest calls StoreSenderRequestFunc.
|
||||||
|
func (mock *RepoMock) StoreSenderRequest(ctx context.Context, req sender.Request) error {
|
||||||
|
if mock.StoreSenderRequestFunc == nil {
|
||||||
|
panic("RepoMock.StoreSenderRequestFunc: method is nil but Repository.StoreSenderRequest was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Ctx context.Context
|
||||||
|
Req sender.Request
|
||||||
|
}{
|
||||||
|
Ctx: ctx,
|
||||||
|
Req: req,
|
||||||
|
}
|
||||||
|
mock.lockStoreSenderRequest.Lock()
|
||||||
|
mock.calls.StoreSenderRequest = append(mock.calls.StoreSenderRequest, callInfo)
|
||||||
|
mock.lockStoreSenderRequest.Unlock()
|
||||||
|
return mock.StoreSenderRequestFunc(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreSenderRequestCalls gets all the calls that were made to StoreSenderRequest.
|
||||||
|
// Check the length with:
|
||||||
|
// len(mockedRepository.StoreSenderRequestCalls())
|
||||||
|
func (mock *RepoMock) StoreSenderRequestCalls() []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
Req sender.Request
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
Req sender.Request
|
||||||
|
}
|
||||||
|
mock.lockStoreSenderRequest.RLock()
|
||||||
|
calls = mock.calls.StoreSenderRequest
|
||||||
|
mock.lockStoreSenderRequest.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
498
pkg/sender/reqlog_mock_test.go
Normal file
498
pkg/sender/reqlog_mock_test.go
Normal file
|
@ -0,0 +1,498 @@
|
||||||
|
// Code generated by moq; DO NOT EDIT.
|
||||||
|
// github.com/matryer/moq
|
||||||
|
|
||||||
|
package sender_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/dstotijn/hetty/pkg/proxy"
|
||||||
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
|
"github.com/oklog/ulid"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure, that ReqLogServiceMock does implement reqlog.Service.
|
||||||
|
// If this is not the case, regenerate this file with moq.
|
||||||
|
var _ reqlog.Service = &ReqLogServiceMock{}
|
||||||
|
|
||||||
|
// ReqLogServiceMock is a mock implementation of reqlog.Service.
|
||||||
|
//
|
||||||
|
// func TestSomethingThatUsesService(t *testing.T) {
|
||||||
|
//
|
||||||
|
// // make and configure a mocked reqlog.Service
|
||||||
|
// mockedService := &ReqLogServiceMock{
|
||||||
|
// ActiveProjectIDFunc: func() ulid.ULID {
|
||||||
|
// panic("mock out the ActiveProjectID method")
|
||||||
|
// },
|
||||||
|
// BypassOutOfScopeRequestsFunc: func() bool {
|
||||||
|
// panic("mock out the BypassOutOfScopeRequests method")
|
||||||
|
// },
|
||||||
|
// ClearRequestsFunc: func(ctx context.Context, projectID ulid.ULID) error {
|
||||||
|
// panic("mock out the ClearRequests method")
|
||||||
|
// },
|
||||||
|
// FindReqsFilterFunc: func() reqlog.FindRequestsFilter {
|
||||||
|
// panic("mock out the FindReqsFilter method")
|
||||||
|
// },
|
||||||
|
// FindRequestLogByIDFunc: func(ctx context.Context, id ulid.ULID) (reqlog.RequestLog, error) {
|
||||||
|
// panic("mock out the FindRequestLogByID method")
|
||||||
|
// },
|
||||||
|
// FindRequestsFunc: func(ctx context.Context) ([]reqlog.RequestLog, error) {
|
||||||
|
// panic("mock out the FindRequests method")
|
||||||
|
// },
|
||||||
|
// RequestModifierFunc: func(next proxy.RequestModifyFunc) proxy.RequestModifyFunc {
|
||||||
|
// panic("mock out the RequestModifier method")
|
||||||
|
// },
|
||||||
|
// ResponseModifierFunc: func(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc {
|
||||||
|
// panic("mock out the ResponseModifier method")
|
||||||
|
// },
|
||||||
|
// SetActiveProjectIDFunc: func(id ulid.ULID) {
|
||||||
|
// panic("mock out the SetActiveProjectID method")
|
||||||
|
// },
|
||||||
|
// SetBypassOutOfScopeRequestsFunc: func(b bool) {
|
||||||
|
// panic("mock out the SetBypassOutOfScopeRequests method")
|
||||||
|
// },
|
||||||
|
// SetFindReqsFilterFunc: func(filter reqlog.FindRequestsFilter) {
|
||||||
|
// panic("mock out the SetFindReqsFilter method")
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // use mockedService in code that requires reqlog.Service
|
||||||
|
// // and then make assertions.
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
type ReqLogServiceMock struct {
|
||||||
|
// ActiveProjectIDFunc mocks the ActiveProjectID method.
|
||||||
|
ActiveProjectIDFunc func() ulid.ULID
|
||||||
|
|
||||||
|
// BypassOutOfScopeRequestsFunc mocks the BypassOutOfScopeRequests method.
|
||||||
|
BypassOutOfScopeRequestsFunc func() bool
|
||||||
|
|
||||||
|
// ClearRequestsFunc mocks the ClearRequests method.
|
||||||
|
ClearRequestsFunc func(ctx context.Context, projectID ulid.ULID) error
|
||||||
|
|
||||||
|
// FindReqsFilterFunc mocks the FindReqsFilter method.
|
||||||
|
FindReqsFilterFunc func() reqlog.FindRequestsFilter
|
||||||
|
|
||||||
|
// FindRequestLogByIDFunc mocks the FindRequestLogByID method.
|
||||||
|
FindRequestLogByIDFunc func(ctx context.Context, id ulid.ULID) (reqlog.RequestLog, error)
|
||||||
|
|
||||||
|
// FindRequestsFunc mocks the FindRequests method.
|
||||||
|
FindRequestsFunc func(ctx context.Context) ([]reqlog.RequestLog, error)
|
||||||
|
|
||||||
|
// RequestModifierFunc mocks the RequestModifier method.
|
||||||
|
RequestModifierFunc func(next proxy.RequestModifyFunc) proxy.RequestModifyFunc
|
||||||
|
|
||||||
|
// ResponseModifierFunc mocks the ResponseModifier method.
|
||||||
|
ResponseModifierFunc func(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc
|
||||||
|
|
||||||
|
// SetActiveProjectIDFunc mocks the SetActiveProjectID method.
|
||||||
|
SetActiveProjectIDFunc func(id ulid.ULID)
|
||||||
|
|
||||||
|
// SetBypassOutOfScopeRequestsFunc mocks the SetBypassOutOfScopeRequests method.
|
||||||
|
SetBypassOutOfScopeRequestsFunc func(b bool)
|
||||||
|
|
||||||
|
// SetFindReqsFilterFunc mocks the SetFindReqsFilter method.
|
||||||
|
SetFindReqsFilterFunc func(filter reqlog.FindRequestsFilter)
|
||||||
|
|
||||||
|
// calls tracks calls to the methods.
|
||||||
|
calls struct {
|
||||||
|
// ActiveProjectID holds details about calls to the ActiveProjectID method.
|
||||||
|
ActiveProjectID []struct {
|
||||||
|
}
|
||||||
|
// BypassOutOfScopeRequests holds details about calls to the BypassOutOfScopeRequests method.
|
||||||
|
BypassOutOfScopeRequests []struct {
|
||||||
|
}
|
||||||
|
// ClearRequests holds details about calls to the ClearRequests method.
|
||||||
|
ClearRequests []struct {
|
||||||
|
// Ctx is the ctx argument value.
|
||||||
|
Ctx context.Context
|
||||||
|
// ProjectID is the projectID argument value.
|
||||||
|
ProjectID ulid.ULID
|
||||||
|
}
|
||||||
|
// FindReqsFilter holds details about calls to the FindReqsFilter method.
|
||||||
|
FindReqsFilter []struct {
|
||||||
|
}
|
||||||
|
// FindRequestLogByID holds details about calls to the FindRequestLogByID method.
|
||||||
|
FindRequestLogByID []struct {
|
||||||
|
// Ctx is the ctx argument value.
|
||||||
|
Ctx context.Context
|
||||||
|
// ID is the id argument value.
|
||||||
|
ID ulid.ULID
|
||||||
|
}
|
||||||
|
// FindRequests holds details about calls to the FindRequests method.
|
||||||
|
FindRequests []struct {
|
||||||
|
// Ctx is the ctx argument value.
|
||||||
|
Ctx context.Context
|
||||||
|
}
|
||||||
|
// RequestModifier holds details about calls to the RequestModifier method.
|
||||||
|
RequestModifier []struct {
|
||||||
|
// Next is the next argument value.
|
||||||
|
Next proxy.RequestModifyFunc
|
||||||
|
}
|
||||||
|
// ResponseModifier holds details about calls to the ResponseModifier method.
|
||||||
|
ResponseModifier []struct {
|
||||||
|
// Next is the next argument value.
|
||||||
|
Next proxy.ResponseModifyFunc
|
||||||
|
}
|
||||||
|
// SetActiveProjectID holds details about calls to the SetActiveProjectID method.
|
||||||
|
SetActiveProjectID []struct {
|
||||||
|
// ID is the id argument value.
|
||||||
|
ID ulid.ULID
|
||||||
|
}
|
||||||
|
// SetBypassOutOfScopeRequests holds details about calls to the SetBypassOutOfScopeRequests method.
|
||||||
|
SetBypassOutOfScopeRequests []struct {
|
||||||
|
// B is the b argument value.
|
||||||
|
B bool
|
||||||
|
}
|
||||||
|
// SetFindReqsFilter holds details about calls to the SetFindReqsFilter method.
|
||||||
|
SetFindReqsFilter []struct {
|
||||||
|
// Filter is the filter argument value.
|
||||||
|
Filter reqlog.FindRequestsFilter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lockActiveProjectID sync.RWMutex
|
||||||
|
lockBypassOutOfScopeRequests sync.RWMutex
|
||||||
|
lockClearRequests sync.RWMutex
|
||||||
|
lockFindReqsFilter sync.RWMutex
|
||||||
|
lockFindRequestLogByID sync.RWMutex
|
||||||
|
lockFindRequests sync.RWMutex
|
||||||
|
lockRequestModifier sync.RWMutex
|
||||||
|
lockResponseModifier sync.RWMutex
|
||||||
|
lockSetActiveProjectID sync.RWMutex
|
||||||
|
lockSetBypassOutOfScopeRequests sync.RWMutex
|
||||||
|
lockSetFindReqsFilter sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActiveProjectID calls ActiveProjectIDFunc.
|
||||||
|
func (mock *ReqLogServiceMock) ActiveProjectID() ulid.ULID {
|
||||||
|
if mock.ActiveProjectIDFunc == nil {
|
||||||
|
panic("ReqLogServiceMock.ActiveProjectIDFunc: method is nil but Service.ActiveProjectID was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
}{}
|
||||||
|
mock.lockActiveProjectID.Lock()
|
||||||
|
mock.calls.ActiveProjectID = append(mock.calls.ActiveProjectID, callInfo)
|
||||||
|
mock.lockActiveProjectID.Unlock()
|
||||||
|
return mock.ActiveProjectIDFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActiveProjectIDCalls gets all the calls that were made to ActiveProjectID.
|
||||||
|
// Check the length with:
|
||||||
|
// len(mockedService.ActiveProjectIDCalls())
|
||||||
|
func (mock *ReqLogServiceMock) ActiveProjectIDCalls() []struct {
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
}
|
||||||
|
mock.lockActiveProjectID.RLock()
|
||||||
|
calls = mock.calls.ActiveProjectID
|
||||||
|
mock.lockActiveProjectID.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// BypassOutOfScopeRequests calls BypassOutOfScopeRequestsFunc.
|
||||||
|
func (mock *ReqLogServiceMock) BypassOutOfScopeRequests() bool {
|
||||||
|
if mock.BypassOutOfScopeRequestsFunc == nil {
|
||||||
|
panic("ReqLogServiceMock.BypassOutOfScopeRequestsFunc: method is nil but Service.BypassOutOfScopeRequests was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
}{}
|
||||||
|
mock.lockBypassOutOfScopeRequests.Lock()
|
||||||
|
mock.calls.BypassOutOfScopeRequests = append(mock.calls.BypassOutOfScopeRequests, callInfo)
|
||||||
|
mock.lockBypassOutOfScopeRequests.Unlock()
|
||||||
|
return mock.BypassOutOfScopeRequestsFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BypassOutOfScopeRequestsCalls gets all the calls that were made to BypassOutOfScopeRequests.
|
||||||
|
// Check the length with:
|
||||||
|
// len(mockedService.BypassOutOfScopeRequestsCalls())
|
||||||
|
func (mock *ReqLogServiceMock) BypassOutOfScopeRequestsCalls() []struct {
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
}
|
||||||
|
mock.lockBypassOutOfScopeRequests.RLock()
|
||||||
|
calls = mock.calls.BypassOutOfScopeRequests
|
||||||
|
mock.lockBypassOutOfScopeRequests.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearRequests calls ClearRequestsFunc.
|
||||||
|
func (mock *ReqLogServiceMock) ClearRequests(ctx context.Context, projectID ulid.ULID) error {
|
||||||
|
if mock.ClearRequestsFunc == nil {
|
||||||
|
panic("ReqLogServiceMock.ClearRequestsFunc: method is nil but Service.ClearRequests was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Ctx context.Context
|
||||||
|
ProjectID ulid.ULID
|
||||||
|
}{
|
||||||
|
Ctx: ctx,
|
||||||
|
ProjectID: projectID,
|
||||||
|
}
|
||||||
|
mock.lockClearRequests.Lock()
|
||||||
|
mock.calls.ClearRequests = append(mock.calls.ClearRequests, callInfo)
|
||||||
|
mock.lockClearRequests.Unlock()
|
||||||
|
return mock.ClearRequestsFunc(ctx, projectID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearRequestsCalls gets all the calls that were made to ClearRequests.
|
||||||
|
// Check the length with:
|
||||||
|
// len(mockedService.ClearRequestsCalls())
|
||||||
|
func (mock *ReqLogServiceMock) ClearRequestsCalls() []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
ProjectID ulid.ULID
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
ProjectID ulid.ULID
|
||||||
|
}
|
||||||
|
mock.lockClearRequests.RLock()
|
||||||
|
calls = mock.calls.ClearRequests
|
||||||
|
mock.lockClearRequests.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindReqsFilter calls FindReqsFilterFunc.
|
||||||
|
func (mock *ReqLogServiceMock) FindReqsFilter() reqlog.FindRequestsFilter {
|
||||||
|
if mock.FindReqsFilterFunc == nil {
|
||||||
|
panic("ReqLogServiceMock.FindReqsFilterFunc: method is nil but Service.FindReqsFilter was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
}{}
|
||||||
|
mock.lockFindReqsFilter.Lock()
|
||||||
|
mock.calls.FindReqsFilter = append(mock.calls.FindReqsFilter, callInfo)
|
||||||
|
mock.lockFindReqsFilter.Unlock()
|
||||||
|
return mock.FindReqsFilterFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindReqsFilterCalls gets all the calls that were made to FindReqsFilter.
|
||||||
|
// Check the length with:
|
||||||
|
// len(mockedService.FindReqsFilterCalls())
|
||||||
|
func (mock *ReqLogServiceMock) FindReqsFilterCalls() []struct {
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
}
|
||||||
|
mock.lockFindReqsFilter.RLock()
|
||||||
|
calls = mock.calls.FindReqsFilter
|
||||||
|
mock.lockFindReqsFilter.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindRequestLogByID calls FindRequestLogByIDFunc.
|
||||||
|
func (mock *ReqLogServiceMock) FindRequestLogByID(ctx context.Context, id ulid.ULID) (reqlog.RequestLog, error) {
|
||||||
|
if mock.FindRequestLogByIDFunc == nil {
|
||||||
|
panic("ReqLogServiceMock.FindRequestLogByIDFunc: method is nil but Service.FindRequestLogByID was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Ctx context.Context
|
||||||
|
ID ulid.ULID
|
||||||
|
}{
|
||||||
|
Ctx: ctx,
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
mock.lockFindRequestLogByID.Lock()
|
||||||
|
mock.calls.FindRequestLogByID = append(mock.calls.FindRequestLogByID, callInfo)
|
||||||
|
mock.lockFindRequestLogByID.Unlock()
|
||||||
|
return mock.FindRequestLogByIDFunc(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindRequestLogByIDCalls gets all the calls that were made to FindRequestLogByID.
|
||||||
|
// Check the length with:
|
||||||
|
// len(mockedService.FindRequestLogByIDCalls())
|
||||||
|
func (mock *ReqLogServiceMock) FindRequestLogByIDCalls() []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
ID ulid.ULID
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
ID ulid.ULID
|
||||||
|
}
|
||||||
|
mock.lockFindRequestLogByID.RLock()
|
||||||
|
calls = mock.calls.FindRequestLogByID
|
||||||
|
mock.lockFindRequestLogByID.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindRequests calls FindRequestsFunc.
|
||||||
|
func (mock *ReqLogServiceMock) FindRequests(ctx context.Context) ([]reqlog.RequestLog, error) {
|
||||||
|
if mock.FindRequestsFunc == nil {
|
||||||
|
panic("ReqLogServiceMock.FindRequestsFunc: method is nil but Service.FindRequests was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Ctx context.Context
|
||||||
|
}{
|
||||||
|
Ctx: ctx,
|
||||||
|
}
|
||||||
|
mock.lockFindRequests.Lock()
|
||||||
|
mock.calls.FindRequests = append(mock.calls.FindRequests, callInfo)
|
||||||
|
mock.lockFindRequests.Unlock()
|
||||||
|
return mock.FindRequestsFunc(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindRequestsCalls gets all the calls that were made to FindRequests.
|
||||||
|
// Check the length with:
|
||||||
|
// len(mockedService.FindRequestsCalls())
|
||||||
|
func (mock *ReqLogServiceMock) FindRequestsCalls() []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
}
|
||||||
|
mock.lockFindRequests.RLock()
|
||||||
|
calls = mock.calls.FindRequests
|
||||||
|
mock.lockFindRequests.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestModifier calls RequestModifierFunc.
|
||||||
|
func (mock *ReqLogServiceMock) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc {
|
||||||
|
if mock.RequestModifierFunc == nil {
|
||||||
|
panic("ReqLogServiceMock.RequestModifierFunc: method is nil but Service.RequestModifier was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Next proxy.RequestModifyFunc
|
||||||
|
}{
|
||||||
|
Next: next,
|
||||||
|
}
|
||||||
|
mock.lockRequestModifier.Lock()
|
||||||
|
mock.calls.RequestModifier = append(mock.calls.RequestModifier, callInfo)
|
||||||
|
mock.lockRequestModifier.Unlock()
|
||||||
|
return mock.RequestModifierFunc(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestModifierCalls gets all the calls that were made to RequestModifier.
|
||||||
|
// Check the length with:
|
||||||
|
// len(mockedService.RequestModifierCalls())
|
||||||
|
func (mock *ReqLogServiceMock) RequestModifierCalls() []struct {
|
||||||
|
Next proxy.RequestModifyFunc
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Next proxy.RequestModifyFunc
|
||||||
|
}
|
||||||
|
mock.lockRequestModifier.RLock()
|
||||||
|
calls = mock.calls.RequestModifier
|
||||||
|
mock.lockRequestModifier.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseModifier calls ResponseModifierFunc.
|
||||||
|
func (mock *ReqLogServiceMock) ResponseModifier(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc {
|
||||||
|
if mock.ResponseModifierFunc == nil {
|
||||||
|
panic("ReqLogServiceMock.ResponseModifierFunc: method is nil but Service.ResponseModifier was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Next proxy.ResponseModifyFunc
|
||||||
|
}{
|
||||||
|
Next: next,
|
||||||
|
}
|
||||||
|
mock.lockResponseModifier.Lock()
|
||||||
|
mock.calls.ResponseModifier = append(mock.calls.ResponseModifier, callInfo)
|
||||||
|
mock.lockResponseModifier.Unlock()
|
||||||
|
return mock.ResponseModifierFunc(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseModifierCalls gets all the calls that were made to ResponseModifier.
|
||||||
|
// Check the length with:
|
||||||
|
// len(mockedService.ResponseModifierCalls())
|
||||||
|
func (mock *ReqLogServiceMock) ResponseModifierCalls() []struct {
|
||||||
|
Next proxy.ResponseModifyFunc
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Next proxy.ResponseModifyFunc
|
||||||
|
}
|
||||||
|
mock.lockResponseModifier.RLock()
|
||||||
|
calls = mock.calls.ResponseModifier
|
||||||
|
mock.lockResponseModifier.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetActiveProjectID calls SetActiveProjectIDFunc.
|
||||||
|
func (mock *ReqLogServiceMock) SetActiveProjectID(id ulid.ULID) {
|
||||||
|
if mock.SetActiveProjectIDFunc == nil {
|
||||||
|
panic("ReqLogServiceMock.SetActiveProjectIDFunc: method is nil but Service.SetActiveProjectID was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
ID ulid.ULID
|
||||||
|
}{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
mock.lockSetActiveProjectID.Lock()
|
||||||
|
mock.calls.SetActiveProjectID = append(mock.calls.SetActiveProjectID, callInfo)
|
||||||
|
mock.lockSetActiveProjectID.Unlock()
|
||||||
|
mock.SetActiveProjectIDFunc(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetActiveProjectIDCalls gets all the calls that were made to SetActiveProjectID.
|
||||||
|
// Check the length with:
|
||||||
|
// len(mockedService.SetActiveProjectIDCalls())
|
||||||
|
func (mock *ReqLogServiceMock) SetActiveProjectIDCalls() []struct {
|
||||||
|
ID ulid.ULID
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
ID ulid.ULID
|
||||||
|
}
|
||||||
|
mock.lockSetActiveProjectID.RLock()
|
||||||
|
calls = mock.calls.SetActiveProjectID
|
||||||
|
mock.lockSetActiveProjectID.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBypassOutOfScopeRequests calls SetBypassOutOfScopeRequestsFunc.
|
||||||
|
func (mock *ReqLogServiceMock) SetBypassOutOfScopeRequests(b bool) {
|
||||||
|
if mock.SetBypassOutOfScopeRequestsFunc == nil {
|
||||||
|
panic("ReqLogServiceMock.SetBypassOutOfScopeRequestsFunc: method is nil but Service.SetBypassOutOfScopeRequests was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
B bool
|
||||||
|
}{
|
||||||
|
B: b,
|
||||||
|
}
|
||||||
|
mock.lockSetBypassOutOfScopeRequests.Lock()
|
||||||
|
mock.calls.SetBypassOutOfScopeRequests = append(mock.calls.SetBypassOutOfScopeRequests, callInfo)
|
||||||
|
mock.lockSetBypassOutOfScopeRequests.Unlock()
|
||||||
|
mock.SetBypassOutOfScopeRequestsFunc(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBypassOutOfScopeRequestsCalls gets all the calls that were made to SetBypassOutOfScopeRequests.
|
||||||
|
// Check the length with:
|
||||||
|
// len(mockedService.SetBypassOutOfScopeRequestsCalls())
|
||||||
|
func (mock *ReqLogServiceMock) SetBypassOutOfScopeRequestsCalls() []struct {
|
||||||
|
B bool
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
B bool
|
||||||
|
}
|
||||||
|
mock.lockSetBypassOutOfScopeRequests.RLock()
|
||||||
|
calls = mock.calls.SetBypassOutOfScopeRequests
|
||||||
|
mock.lockSetBypassOutOfScopeRequests.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFindReqsFilter calls SetFindReqsFilterFunc.
|
||||||
|
func (mock *ReqLogServiceMock) SetFindReqsFilter(filter reqlog.FindRequestsFilter) {
|
||||||
|
if mock.SetFindReqsFilterFunc == nil {
|
||||||
|
panic("ReqLogServiceMock.SetFindReqsFilterFunc: method is nil but Service.SetFindReqsFilter was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Filter reqlog.FindRequestsFilter
|
||||||
|
}{
|
||||||
|
Filter: filter,
|
||||||
|
}
|
||||||
|
mock.lockSetFindReqsFilter.Lock()
|
||||||
|
mock.calls.SetFindReqsFilter = append(mock.calls.SetFindReqsFilter, callInfo)
|
||||||
|
mock.lockSetFindReqsFilter.Unlock()
|
||||||
|
mock.SetFindReqsFilterFunc(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFindReqsFilterCalls gets all the calls that were made to SetFindReqsFilter.
|
||||||
|
// Check the length with:
|
||||||
|
// len(mockedService.SetFindReqsFilterCalls())
|
||||||
|
func (mock *ReqLogServiceMock) SetFindReqsFilterCalls() []struct {
|
||||||
|
Filter reqlog.FindRequestsFilter
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Filter reqlog.FindRequestsFilter
|
||||||
|
}
|
||||||
|
mock.lockSetFindReqsFilter.RLock()
|
||||||
|
calls = mock.calls.SetFindReqsFilter
|
||||||
|
mock.lockSetFindReqsFilter.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
228
pkg/sender/search.go
Normal file
228
pkg/sender/search.go
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
package sender
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid"
|
||||||
|
|
||||||
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
|
"github.com/dstotijn/hetty/pkg/search"
|
||||||
|
)
|
||||||
|
|
||||||
|
var senderReqSearchKeyFns = map[string]func(req Request) string{
|
||||||
|
"req.id": func(req Request) string { return req.ID.String() },
|
||||||
|
"req.proto": func(req Request) string { return req.Proto },
|
||||||
|
"req.url": func(req Request) string {
|
||||||
|
if req.URL == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return req.URL.String()
|
||||||
|
},
|
||||||
|
"req.method": func(req Request) string { return req.Method },
|
||||||
|
"req.body": func(req Request) string { return string(req.Body) },
|
||||||
|
"req.timestamp": func(req Request) string { return ulid.Time(req.ID.Time()).String() },
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Request and response headers search key functions.
|
||||||
|
|
||||||
|
// Matches returns true if the supplied search expression evaluates to true.
|
||||||
|
func (req Request) Matches(expr search.Expression) (bool, error) {
|
||||||
|
switch e := expr.(type) {
|
||||||
|
case search.PrefixExpression:
|
||||||
|
return req.matchPrefixExpr(e)
|
||||||
|
case search.InfixExpression:
|
||||||
|
return req.matchInfixExpr(e)
|
||||||
|
case search.StringLiteral:
|
||||||
|
return req.matchStringLiteral(e)
|
||||||
|
default:
|
||||||
|
return false, fmt.Errorf("expression type (%T) not supported", expr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req Request) matchPrefixExpr(expr search.PrefixExpression) (bool, error) {
|
||||||
|
switch expr.Operator {
|
||||||
|
case search.TokOpNot:
|
||||||
|
match, err := req.Matches(expr.Right)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return !match, nil
|
||||||
|
default:
|
||||||
|
return false, errors.New("operator is not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req Request) matchInfixExpr(expr search.InfixExpression) (bool, error) {
|
||||||
|
switch expr.Operator {
|
||||||
|
case search.TokOpAnd:
|
||||||
|
left, err := req.Matches(expr.Left)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
right, err := req.Matches(expr.Right)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return left && right, nil
|
||||||
|
case search.TokOpOr:
|
||||||
|
left, err := req.Matches(expr.Left)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
right, err := req.Matches(expr.Right)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return left || right, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
left, ok := expr.Left.(search.StringLiteral)
|
||||||
|
if !ok {
|
||||||
|
return false, errors.New("left operand must be a string literal")
|
||||||
|
}
|
||||||
|
|
||||||
|
leftVal := req.getMappedStringLiteral(left.Value)
|
||||||
|
|
||||||
|
if expr.Operator == search.TokOpRe || expr.Operator == search.TokOpNotRe {
|
||||||
|
right, ok := expr.Right.(*regexp.Regexp)
|
||||||
|
if !ok {
|
||||||
|
return false, errors.New("right operand must be a regular expression")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch expr.Operator {
|
||||||
|
case search.TokOpRe:
|
||||||
|
return right.MatchString(leftVal), nil
|
||||||
|
case search.TokOpNotRe:
|
||||||
|
return !right.MatchString(leftVal), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
right, ok := expr.Right.(search.StringLiteral)
|
||||||
|
if !ok {
|
||||||
|
return false, errors.New("right operand must be a string literal")
|
||||||
|
}
|
||||||
|
|
||||||
|
rightVal := req.getMappedStringLiteral(right.Value)
|
||||||
|
|
||||||
|
switch expr.Operator {
|
||||||
|
case search.TokOpEq:
|
||||||
|
return leftVal == rightVal, nil
|
||||||
|
case search.TokOpNotEq:
|
||||||
|
return leftVal != rightVal, nil
|
||||||
|
case search.TokOpGt:
|
||||||
|
// TODO(?) attempt to parse as int.
|
||||||
|
return leftVal > rightVal, nil
|
||||||
|
case search.TokOpLt:
|
||||||
|
// TODO(?) attempt to parse as int.
|
||||||
|
return leftVal < rightVal, nil
|
||||||
|
case search.TokOpGtEq:
|
||||||
|
// TODO(?) attempt to parse as int.
|
||||||
|
return leftVal >= rightVal, nil
|
||||||
|
case search.TokOpLtEq:
|
||||||
|
// TODO(?) attempt to parse as int.
|
||||||
|
return leftVal <= rightVal, nil
|
||||||
|
default:
|
||||||
|
return false, errors.New("unsupported operator")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req Request) getMappedStringLiteral(s string) string {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(s, "req."):
|
||||||
|
fn, ok := senderReqSearchKeyFns[s]
|
||||||
|
if ok {
|
||||||
|
return fn(req)
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(s, "res."):
|
||||||
|
if req.Response == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fn, ok := reqlog.ResLogSearchKeyFns[s]
|
||||||
|
if ok {
|
||||||
|
return fn(*req.Response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req Request) matchStringLiteral(strLiteral search.StringLiteral) (bool, error) {
|
||||||
|
for _, fn := range senderReqSearchKeyFns {
|
||||||
|
if strings.Contains(
|
||||||
|
strings.ToLower(fn(req)),
|
||||||
|
strings.ToLower(strLiteral.Value),
|
||||||
|
) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Response != nil {
|
||||||
|
for _, fn := range reqlog.ResLogSearchKeyFns {
|
||||||
|
if strings.Contains(
|
||||||
|
strings.ToLower(fn(*req.Response)),
|
||||||
|
strings.ToLower(strLiteral.Value),
|
||||||
|
) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req Request) MatchScope(s *scope.Scope) bool {
|
||||||
|
for _, rule := range s.Rules() {
|
||||||
|
if rule.URL != nil && req.URL != nil {
|
||||||
|
if matches := rule.URL.MatchString(req.URL.String()); matches {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, values := range req.Header {
|
||||||
|
var keyMatches, valueMatches bool
|
||||||
|
|
||||||
|
if rule.Header.Key != nil {
|
||||||
|
if matches := rule.Header.Key.MatchString(key); matches {
|
||||||
|
keyMatches = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.Header.Value != nil {
|
||||||
|
for _, value := range values {
|
||||||
|
if matches := rule.Header.Value.MatchString(value); matches {
|
||||||
|
valueMatches = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// When only key or value is set, match on whatever is set.
|
||||||
|
// When both are set, both must match.
|
||||||
|
switch {
|
||||||
|
case rule.Header.Key != nil && rule.Header.Value == nil && keyMatches:
|
||||||
|
return true
|
||||||
|
case rule.Header.Key == nil && rule.Header.Value != nil && valueMatches:
|
||||||
|
return true
|
||||||
|
case rule.Header.Key != nil && rule.Header.Value != nil && keyMatches && valueMatches:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.Body != nil {
|
||||||
|
if matches := rule.Body.Match(req.Body); matches {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
204
pkg/sender/search_test.go
Normal file
204
pkg/sender/search_test.go
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
package sender_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
|
"github.com/dstotijn/hetty/pkg/search"
|
||||||
|
"github.com/dstotijn/hetty/pkg/sender"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRequestLogMatch(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
query string
|
||||||
|
senderReq sender.Request
|
||||||
|
expectedMatch bool
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "infix expression, equal operator, match",
|
||||||
|
query: "req.body = foo",
|
||||||
|
senderReq: sender.Request{
|
||||||
|
Body: []byte("foo"),
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "infix expression, not equal operator, match",
|
||||||
|
query: "req.body != bar",
|
||||||
|
senderReq: sender.Request{
|
||||||
|
Body: []byte("foo"),
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "infix expression, greater than operator, match",
|
||||||
|
query: "req.body > a",
|
||||||
|
senderReq: sender.Request{
|
||||||
|
Body: []byte("b"),
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "infix expression, less than operator, match",
|
||||||
|
query: "req.body < b",
|
||||||
|
senderReq: sender.Request{
|
||||||
|
Body: []byte("a"),
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "infix expression, greater than or equal operator, match greater than",
|
||||||
|
query: "req.body >= a",
|
||||||
|
senderReq: sender.Request{
|
||||||
|
Body: []byte("b"),
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "infix expression, greater than or equal operator, match equal",
|
||||||
|
query: "req.body >= a",
|
||||||
|
senderReq: sender.Request{
|
||||||
|
Body: []byte("a"),
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "infix expression, less than or equal operator, match less than",
|
||||||
|
query: "req.body <= b",
|
||||||
|
senderReq: sender.Request{
|
||||||
|
Body: []byte("a"),
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "infix expression, less than or equal operator, match equal",
|
||||||
|
query: "req.body <= b",
|
||||||
|
senderReq: sender.Request{
|
||||||
|
Body: []byte("b"),
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "infix expression, regular expression operator, match",
|
||||||
|
query: `req.body =~ "^foo(.*)$"`,
|
||||||
|
senderReq: sender.Request{
|
||||||
|
Body: []byte("foobar"),
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "infix expression, negate regular expression operator, match",
|
||||||
|
query: `req.body !~ "^foo(.*)$"`,
|
||||||
|
senderReq: sender.Request{
|
||||||
|
Body: []byte("xoobar"),
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "infix expression, and operator, match",
|
||||||
|
query: "req.body = bar AND res.body = yolo",
|
||||||
|
senderReq: sender.Request{
|
||||||
|
Body: []byte("bar"),
|
||||||
|
Response: &reqlog.ResponseLog{
|
||||||
|
Body: []byte("yolo"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "infix expression, or operator, match",
|
||||||
|
query: "req.body = bar OR res.body = yolo",
|
||||||
|
senderReq: sender.Request{
|
||||||
|
Body: []byte("foo"),
|
||||||
|
Response: &reqlog.ResponseLog{
|
||||||
|
Body: []byte("yolo"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prefix expression, not operator, match",
|
||||||
|
query: "NOT (req.body = bar)",
|
||||||
|
senderReq: sender.Request{
|
||||||
|
Body: []byte("foo"),
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "string literal expression, match in request log",
|
||||||
|
query: "foo",
|
||||||
|
senderReq: sender.Request{
|
||||||
|
Body: []byte("foo"),
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "string literal expression, no match",
|
||||||
|
query: "foo",
|
||||||
|
senderReq: sender.Request{
|
||||||
|
Body: []byte("bar"),
|
||||||
|
},
|
||||||
|
expectedMatch: false,
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "string literal expression, match in response log",
|
||||||
|
query: "foo",
|
||||||
|
senderReq: sender.Request{
|
||||||
|
Response: &reqlog.ResponseLog{
|
||||||
|
Body: []byte("foo"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
searchExpr, err := search.ParseQuery(tt.query)
|
||||||
|
assertError(t, nil, err)
|
||||||
|
|
||||||
|
got, err := tt.senderReq.Matches(searchExpr)
|
||||||
|
assertError(t, tt.expectedError, err)
|
||||||
|
|
||||||
|
if tt.expectedMatch != got {
|
||||||
|
t.Errorf("expected match result: %v, got: %v", tt.expectedMatch, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertError(t *testing.T, exp, got error) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case exp == nil && got != nil:
|
||||||
|
t.Fatalf("expected: nil, got: %v", got)
|
||||||
|
case exp != nil && got == nil:
|
||||||
|
t.Fatalf("expected: %v, got: nil", exp.Error())
|
||||||
|
case exp != nil && got != nil && exp.Error() != got.Error():
|
||||||
|
t.Fatalf("expected: %v, got: %v", exp.Error(), got.Error())
|
||||||
|
}
|
||||||
|
}
|
251
pkg/sender/sender.go
Normal file
251
pkg/sender/sender.go
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
package sender
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid"
|
||||||
|
|
||||||
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
|
"github.com/dstotijn/hetty/pkg/search"
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint:gosec
|
||||||
|
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
|
var defaultHTTPClient = &http.Client{
|
||||||
|
Transport: &HTTPTransport{},
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrProjectIDMustBeSet = errors.New("sender: project ID must be set")
|
||||||
|
ErrRequestNotFound = errors.New("sender: request not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
FindRequestByID(ctx context.Context, id ulid.ULID) (Request, error)
|
||||||
|
FindRequests(ctx context.Context) ([]Request, error)
|
||||||
|
CreateOrUpdateRequest(ctx context.Context, req Request) (Request, error)
|
||||||
|
CloneFromRequestLog(ctx context.Context, reqLogID ulid.ULID) (Request, error)
|
||||||
|
DeleteRequests(ctx context.Context, projectID ulid.ULID) error
|
||||||
|
SendRequest(ctx context.Context, id ulid.ULID) (Request, error)
|
||||||
|
SetActiveProjectID(ulid.ULID)
|
||||||
|
SetFindReqsFilter(filter FindRequestsFilter)
|
||||||
|
FindReqsFilter() FindRequestsFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
activeProjectID ulid.ULID
|
||||||
|
findReqsFilter FindRequestsFilter
|
||||||
|
scope *scope.Scope
|
||||||
|
repo Repository
|
||||||
|
reqLogSvc reqlog.Service
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type FindRequestsFilter struct {
|
||||||
|
ProjectID ulid.ULID
|
||||||
|
OnlyInScope bool
|
||||||
|
SearchExpr search.Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Scope *scope.Scope
|
||||||
|
Repository Repository
|
||||||
|
ReqLogService reqlog.Service
|
||||||
|
HTTPClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type SendError struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(cfg Config) Service {
|
||||||
|
svc := &service{
|
||||||
|
repo: cfg.Repository,
|
||||||
|
reqLogSvc: cfg.ReqLogService,
|
||||||
|
httpClient: defaultHTTPClient,
|
||||||
|
scope: cfg.Scope,
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.HTTPClient != nil {
|
||||||
|
svc.httpClient = cfg.HTTPClient
|
||||||
|
}
|
||||||
|
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
ID ulid.ULID
|
||||||
|
ProjectID ulid.ULID
|
||||||
|
SourceRequestLogID ulid.ULID
|
||||||
|
|
||||||
|
URL *url.URL
|
||||||
|
Method string
|
||||||
|
Proto string
|
||||||
|
Header http.Header
|
||||||
|
Body []byte
|
||||||
|
|
||||||
|
Response *reqlog.ResponseLog
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *service) FindRequestByID(ctx context.Context, id ulid.ULID) (Request, error) {
|
||||||
|
req, err := svc.repo.FindSenderRequestByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return Request{}, fmt.Errorf("sender: failed to find request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *service) FindRequests(ctx context.Context) ([]Request, error) {
|
||||||
|
return svc.repo.FindSenderRequests(ctx, svc.findReqsFilter, svc.scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *service) CreateOrUpdateRequest(ctx context.Context, req Request) (Request, error) {
|
||||||
|
if svc.activeProjectID.Compare(ulid.ULID{}) == 0 {
|
||||||
|
return Request{}, ErrProjectIDMustBeSet
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ID.Compare(ulid.ULID{}) == 0 {
|
||||||
|
req.ID = ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.ProjectID = svc.activeProjectID
|
||||||
|
|
||||||
|
if req.Method == "" {
|
||||||
|
req.Method = http.MethodGet
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Proto == "" {
|
||||||
|
req.Proto = HTTPProto2
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValidProto(req.Proto) {
|
||||||
|
return Request{}, fmt.Errorf("sender: unsupported HTTP protocol: %v", req.Proto)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := svc.repo.StoreSenderRequest(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return Request{}, fmt.Errorf("sender: failed to store request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *service) CloneFromRequestLog(ctx context.Context, reqLogID ulid.ULID) (Request, error) {
|
||||||
|
if svc.activeProjectID.Compare(ulid.ULID{}) == 0 {
|
||||||
|
return Request{}, ErrProjectIDMustBeSet
|
||||||
|
}
|
||||||
|
|
||||||
|
reqLog, err := svc.reqLogSvc.FindRequestLogByID(ctx, reqLogID)
|
||||||
|
if err != nil {
|
||||||
|
return Request{}, fmt.Errorf("sender: failed to find request log: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := Request{
|
||||||
|
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
|
||||||
|
ProjectID: svc.activeProjectID,
|
||||||
|
SourceRequestLogID: reqLogID,
|
||||||
|
Method: reqLog.Method,
|
||||||
|
URL: reqLog.URL,
|
||||||
|
Proto: HTTPProto2, // Attempt HTTP/2.
|
||||||
|
Header: reqLog.Header,
|
||||||
|
Body: reqLog.Body,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = svc.repo.StoreSenderRequest(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return Request{}, fmt.Errorf("sender: failed to store request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *service) SetFindReqsFilter(filter FindRequestsFilter) {
|
||||||
|
svc.findReqsFilter = filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *service) FindReqsFilter() FindRequestsFilter {
|
||||||
|
return svc.findReqsFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *service) SendRequest(ctx context.Context, id ulid.ULID) (Request, error) {
|
||||||
|
req, err := svc.repo.FindSenderRequestByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return Request{}, fmt.Errorf("sender: failed to find request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpReq, err := parseHTTPRequest(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return Request{}, fmt.Errorf("sender: failed to parse HTTP request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resLog, err := svc.sendHTTPRequest(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return Request{}, fmt.Errorf("sender: could not send HTTP request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = svc.repo.StoreResponseLog(ctx, id, resLog)
|
||||||
|
if err != nil {
|
||||||
|
return Request{}, fmt.Errorf("sender: failed to store sender response log: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Response = &resLog
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHTTPRequest(ctx context.Context, req Request) (*http.Request, error) {
|
||||||
|
ctx = context.WithValue(ctx, protoCtxKey{}, req.Proto)
|
||||||
|
|
||||||
|
httpReq, err := http.NewRequestWithContext(ctx, req.Method, req.URL.String(), bytes.NewReader(req.Body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to construct HTTP request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Header != nil {
|
||||||
|
httpReq.Header = req.Header
|
||||||
|
}
|
||||||
|
|
||||||
|
return httpReq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *service) sendHTTPRequest(httpReq *http.Request) (reqlog.ResponseLog, error) {
|
||||||
|
res, err := svc.httpClient.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return reqlog.ResponseLog{}, &SendError{err}
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
resLog, err := reqlog.ParseHTTPResponse(res)
|
||||||
|
if err != nil {
|
||||||
|
return reqlog.ResponseLog{}, fmt.Errorf("failed to parse http response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resLog, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *service) SetActiveProjectID(id ulid.ULID) {
|
||||||
|
svc.activeProjectID = id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *service) DeleteRequests(ctx context.Context, projectID ulid.ULID) error {
|
||||||
|
return svc.repo.DeleteSenderRequests(ctx, projectID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e SendError) Error() string {
|
||||||
|
return fmt.Sprintf("failed to send HTTP request: %v", e.err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e SendError) Unwrap() error {
|
||||||
|
return e.err
|
||||||
|
}
|
289
pkg/sender/sender_test.go
Normal file
289
pkg/sender/sender_test.go
Normal file
|
@ -0,0 +1,289 @@
|
||||||
|
package sender_test
|
||||||
|
|
||||||
|
//go:generate go run github.com/matryer/moq -out reqlog_mock_test.go -pkg sender_test ../reqlog Service:ReqLogServiceMock
|
||||||
|
//go:generate go run github.com/matryer/moq -out repo_mock_test.go -pkg sender_test . Repository:RepoMock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/oklog/ulid"
|
||||||
|
|
||||||
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
|
"github.com/dstotijn/hetty/pkg/sender"
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint:gosec
|
||||||
|
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
|
var exampleURL = func() *url.URL {
|
||||||
|
u, err := url.Parse("https://example.com/foobar")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return u
|
||||||
|
}()
|
||||||
|
|
||||||
|
func TestStoreRequest(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("without active project", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
svc := sender.NewService(sender.Config{})
|
||||||
|
|
||||||
|
_, err := svc.CreateOrUpdateRequest(context.Background(), sender.Request{
|
||||||
|
URL: exampleURL,
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Body: []byte("foobar"),
|
||||||
|
})
|
||||||
|
if !errors.Is(err, sender.ErrProjectIDMustBeSet) {
|
||||||
|
t.Fatalf("expected `sender.ErrProjectIDMustBeSet`, got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("with active project", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
repoMock := &RepoMock{
|
||||||
|
StoreSenderRequestFunc: func(ctx context.Context, req sender.Request) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
svc := sender.NewService(sender.Config{
|
||||||
|
Repository: repoMock,
|
||||||
|
})
|
||||||
|
|
||||||
|
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
||||||
|
svc.SetActiveProjectID(projectID)
|
||||||
|
|
||||||
|
exp := sender.Request{
|
||||||
|
ProjectID: projectID,
|
||||||
|
URL: exampleURL,
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Foo": []string{"bar"},
|
||||||
|
},
|
||||||
|
Body: []byte("foobar"),
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := svc.CreateOrUpdateRequest(context.Background(), sender.Request{
|
||||||
|
URL: exampleURL,
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Foo": []string{"bar"},
|
||||||
|
},
|
||||||
|
Body: []byte("foobar"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error storing request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got.ID.Compare(ulid.ULID{}) == 0 {
|
||||||
|
t.Fatal("expected request ID to be non-empty value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(repoMock.StoreSenderRequestCalls()) != 1 {
|
||||||
|
t.Fatal("expected `svc.repo.StoreSenderRequest()` to have been called 1 time")
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(got, repoMock.StoreSenderRequestCalls()[0].Req); diff != "" {
|
||||||
|
t.Fatalf("repo call arg not equal (-exp, +got):\n%v", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset ID to make comparison with expected easier.
|
||||||
|
got.ID = ulid.ULID{}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(exp, got); diff != "" {
|
||||||
|
t.Fatalf("request not equal (-exp, +got):\n%v", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloneFromRequestLog(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
reqLogID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
||||||
|
|
||||||
|
t.Run("without active project", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
svc := sender.NewService(sender.Config{})
|
||||||
|
|
||||||
|
_, err := svc.CloneFromRequestLog(context.Background(), reqLogID)
|
||||||
|
if !errors.Is(err, sender.ErrProjectIDMustBeSet) {
|
||||||
|
t.Fatalf("expected `sender.ErrProjectIDMustBeSet`, got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("with active project", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
reqLog := reqlog.RequestLog{
|
||||||
|
ID: reqLogID,
|
||||||
|
ProjectID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
|
||||||
|
URL: exampleURL,
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Foo": []string{"bar"},
|
||||||
|
},
|
||||||
|
Body: []byte("foobar"),
|
||||||
|
}
|
||||||
|
|
||||||
|
reqLogMock := &ReqLogServiceMock{
|
||||||
|
FindRequestLogByIDFunc: func(ctx context.Context, id ulid.ULID) (reqlog.RequestLog, error) {
|
||||||
|
return reqLog, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
repoMock := &RepoMock{
|
||||||
|
StoreSenderRequestFunc: func(ctx context.Context, req sender.Request) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
svc := sender.NewService(sender.Config{
|
||||||
|
ReqLogService: reqLogMock,
|
||||||
|
Repository: repoMock,
|
||||||
|
})
|
||||||
|
|
||||||
|
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
||||||
|
svc.SetActiveProjectID(projectID)
|
||||||
|
|
||||||
|
exp := sender.Request{
|
||||||
|
SourceRequestLogID: reqLogID,
|
||||||
|
ProjectID: projectID,
|
||||||
|
URL: exampleURL,
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Proto: sender.HTTPProto2,
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Foo": []string{"bar"},
|
||||||
|
},
|
||||||
|
Body: []byte("foobar"),
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := svc.CloneFromRequestLog(context.Background(), reqLogID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error cloning from request log: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(reqLogMock.FindRequestLogByIDCalls()) != 1 {
|
||||||
|
t.Fatal("expected `svc.reqLogSvc.FindRequestLogByID()` to have been called 1 time")
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := reqLogMock.FindRequestLogByIDCalls()[0].ID; reqLogID.Compare(got) != 0 {
|
||||||
|
t.Fatalf("reqlog service call arg `id` not equal (expected: %q, got: %q)", reqLogID, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got.ID.Compare(ulid.ULID{}) == 0 {
|
||||||
|
t.Fatal("expected request ID to be non-empty value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(repoMock.StoreSenderRequestCalls()) != 1 {
|
||||||
|
t.Fatal("expected `svc.repo.StoreSenderRequest()` to have been called 1 time")
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(got, repoMock.StoreSenderRequestCalls()[0].Req); diff != "" {
|
||||||
|
t.Fatalf("repo call arg not equal (-exp, +got):\n%v", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset ID to make comparison with expected easier.
|
||||||
|
got.ID = ulid.ULID{}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(exp, got); diff != "" {
|
||||||
|
t.Fatalf("request not equal (-exp, +got):\n%v", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendRequest(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
date := time.Now().Format(http.TimeFormat)
|
||||||
|
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Foobar", "baz")
|
||||||
|
w.Header().Set("Date", date)
|
||||||
|
fmt.Fprint(w, "baz")
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
tsURL, _ := url.Parse(ts.URL)
|
||||||
|
|
||||||
|
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
||||||
|
req := sender.Request{
|
||||||
|
ID: reqID,
|
||||||
|
ProjectID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
|
||||||
|
URL: tsURL,
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Foo": []string{"bar"},
|
||||||
|
},
|
||||||
|
Body: []byte("foobar"),
|
||||||
|
}
|
||||||
|
|
||||||
|
repoMock := &RepoMock{
|
||||||
|
FindSenderRequestByIDFunc: func(ctx context.Context, id ulid.ULID) (sender.Request, error) {
|
||||||
|
return req, nil
|
||||||
|
},
|
||||||
|
StoreResponseLogFunc: func(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
svc := sender.NewService(sender.Config{
|
||||||
|
Repository: repoMock,
|
||||||
|
})
|
||||||
|
|
||||||
|
exp := reqlog.ResponseLog{
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Status: "200 OK",
|
||||||
|
Header: http.Header{
|
||||||
|
"Content-Length": []string{"3"},
|
||||||
|
"Content-Type": []string{"text/plain; charset=utf-8"},
|
||||||
|
"Date": []string{date},
|
||||||
|
"Foobar": []string{"baz"},
|
||||||
|
},
|
||||||
|
Body: []byte("baz"),
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := svc.SendRequest(context.Background(), reqID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error sending request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(repoMock.FindSenderRequestByIDCalls()) != 1 {
|
||||||
|
t.Fatal("expected `svc.repo.FindSenderRequestByID()` to have been called 1 time")
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(reqID, repoMock.FindSenderRequestByIDCalls()[0].ID); diff != "" {
|
||||||
|
t.Fatalf("call arg `id` for `svc.repo.FindSenderRequestByID()` not equal (-exp, +got):\n%v", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(repoMock.StoreResponseLogCalls()) != 1 {
|
||||||
|
t.Fatal("expected `svc.repo.StoreResponseLog()` to have been called 1 time")
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(reqID, repoMock.StoreResponseLogCalls()[0].ReqLogID); diff != "" {
|
||||||
|
t.Fatalf("call arg `reqLogID` for `svc.repo.StoreResponseLog()` not equal (-exp, +got):\n%v", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(exp, repoMock.StoreResponseLogCalls()[0].ResLog); diff != "" {
|
||||||
|
t.Fatalf("call arg `resLog` for `svc.repo.StoreResponseLog()` not equal (-exp, +got):\n%v", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(repoMock.StoreResponseLogCalls()[0].ResLog, *got.Response); diff != "" {
|
||||||
|
t.Fatalf("returned response log value and persisted value not equal (-exp, +got):\n%v", diff)
|
||||||
|
}
|
||||||
|
}
|
50
pkg/sender/transport.go
Normal file
50
pkg/sender/transport.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package sender
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HTTPTransport struct{}
|
||||||
|
|
||||||
|
type protoCtxKey struct{}
|
||||||
|
|
||||||
|
const (
|
||||||
|
HTTPProto1 = "HTTP/1.1"
|
||||||
|
HTTPProto2 = "HTTP/2.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
// h1OnlyTransport mimics `http.DefaultTransport`, but with HTTP/2 disabled.
|
||||||
|
var h1OnlyTransport = &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}).DialContext,
|
||||||
|
MaxIdleConns: 100,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
|
||||||
|
// Disable HTTP/2.
|
||||||
|
TLSNextProto: map[string]func(string, *tls.Conn) http.RoundTripper{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// RountTrip implements http.RoundTripper. Based on a context value on the
|
||||||
|
// HTTP request, it switches between using `http.DefaultTransport` (which attempts
|
||||||
|
// HTTP/2) and a HTTP/1.1 only transport that's based off `http.DefaultTransport`.
|
||||||
|
func (t *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
proto, ok := req.Context().Value(protoCtxKey{}).(string)
|
||||||
|
|
||||||
|
if ok && proto == HTTPProto1 {
|
||||||
|
return h1OnlyTransport.RoundTrip(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.DefaultTransport.RoundTrip(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidProto(proto string) bool {
|
||||||
|
return proto == HTTPProto1 || proto == HTTPProto2
|
||||||
|
}
|
Loading…
Reference in a new issue