Skip to content

Commit

Permalink
feat: remove api key
Browse files Browse the repository at this point in the history
  • Loading branch information
svedova committed Oct 7, 2023
1 parent eafb8e2 commit f87f155
Show file tree
Hide file tree
Showing 7 changed files with 268 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { Scope } from "nock";
import { RenderResult, waitFor } from "@testing-library/react";
import { fireEvent, render } from "@testing-library/react";
import mockApp from "~/testing/data/mock_app";
import mockEnvironments from "~/testing/data/mock_environments";
import {
mockFetchAPIKeys,
mockDeleteAPIKey,
} from "~/testing/nocks/nock_api_keys";
import TabAPIKey from "./TabAPIKey";

interface WrapperProps {
app?: App;
environments?: Environment[];
hash?: string;
setRefreshToken?: () => void;
}

describe("~/pages/apps/[id]/environments/[env-id]/config/_components/TabAPIKey.tsx", () => {
let wrapper: RenderResult;
let fetchScope: Scope;
let currentApp: App;
let currentEnv: Environment;
let currentEnvs: Environment[];

const createWrapper = ({ app, environments }: WrapperProps) => {
currentApp = app || mockApp();
currentApp.id = "1644802351"; // Same as api key id

currentEnvs = environments || mockEnvironments({ app: currentApp });
currentEnv = currentEnvs[0];

fetchScope = mockFetchAPIKeys({
appId: currentApp.id,
envId: currentEnv.id!,
});

wrapper = render(<TabAPIKey app={currentApp} environment={currentEnv} />);
};

test("should fetch api keys", async () => {
createWrapper({});

await waitFor(() => {
expect(fetchScope.isDone()).toBe(true);
});

const subheader =
"This key will allow you to interact with our API and modify this environment.";

// Header
expect(wrapper.getByText("API Key")).toBeTruthy();
expect(wrapper.getByText(subheader)).toBeTruthy();

// API Keys
expect(wrapper.getByText("Default")).toBeTruthy();
expect(wrapper.getByText("CI")).toBeTruthy();
});

test("should delete api key", async () => {
createWrapper({});

await waitFor(() => {
expect(wrapper.getByText("CI")).toBeTruthy();
});

fireEvent.click(wrapper.getAllByLabelText("Remove API Key").at(1)!);

await waitFor(() => {
expect(wrapper.getByText("Confirm action")).toBeTruthy();
});

const scope = mockDeleteAPIKey({
appId: currentApp.id,
envId: currentEnv.id!,
keyId: "9868814106",
});

// Should refetch api keys
const fetchScope2 = mockFetchAPIKeys({
appId: currentApp.id,
envId: currentEnv.id!,
response: { keys: [] },
});

fireEvent.click(wrapper.getByText("Yes, continue"));

await waitFor(() => {
expect(scope.isDone()).toBe(true);
expect(fetchScope2.isDone()).toBe(true);
// Should close modal
expect(() => wrapper.getByText("Confirm action")).toThrow();
// Should no longer have API keys
expect(() => wrapper.getByText("Default")).toThrow();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ import Button from "@mui/lab/LoadingButton";
import TextInput from "@mui/material/TextField";
import VisibilityIcon from "@mui/icons-material/Visibility";
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
import DeleteIcon from "@mui/icons-material/Delete";
import Modal from "~/components/ModalV2";
import ConfirmModal from "~/components/ConfirmModal";
import Spinner from "~/components/Spinner";
import InputDescription from "~/components/InputDescription";
import { useFetchAPIKeys, generateNewAPIKey } from "../actions";
import { useFetchAPIKeys, generateNewAPIKey, deleteAPIKey } from "../actions";

interface Props {
app: App;
environment: Environment;
setRefreshToken: (v: number) => void;
setRefreshToken?: (v: number) => void; // This is there just to accomodate the Tab signature. It's not really used.
}

interface APIKeyModalProps {
Expand Down Expand Up @@ -72,13 +74,16 @@ function APIKeyModal({ onSubmit, onClose, loading, error }: APIKeyModalProps) {

export default function TabAPIKey({ app, environment: env }: Props) {
const [isVisible, setIsVisible] = useState<string>("");
const [refreshToken, setRefreshToken] = useState<number>();
const [success, setSuccess] = useState(false);
const [modalError, setModalError] = useState("");
const [modalLoading, setModalLoading] = useState(false);
const [apiKeyToDelete, setApiKeyToDelete] = useState<APIKey>();
const [isModalOpen, setIsModalOpen] = useState(false);
const { loading, error, keys, setKeys } = useFetchAPIKeys({
appId: app.id,
envId: env.id!,
refreshToken,
});

const handleNewKey = (name: string) => {
Expand Down Expand Up @@ -146,33 +151,66 @@ export default function TabAPIKey({ app, environment: env }: Props) {
{keys.map(apiKey => (
<Box
key={apiKey.token}
sx={{ mb: 2, bgcolor: "rgba(0,0,0,0.1)", p: 2 }}
sx={{
mb: 2,
bgcolor: "rgba(0,0,0,0.1)",
p: 2,
}}
>
<Typography>{apiKey.name}</Typography>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Box
sx={{
textOverflow: "ellipsis",
overflow: "hidden",
maxWidth: { md: "300px", lg: "none" },
}}
>
{isVisible === apiKey.id ? apiKey.token : "*".repeat(32)}
<Box>
<Typography>{apiKey.name}</Typography>
<Box
sx={{
textOverflow: "ellipsis",
overflow: "hidden",
maxWidth: { md: "300px", lg: "none" },
}}
>
{isVisible === apiKey.id ? apiKey.token : "*".repeat(32)}
</Box>
</Box>
<Box>
<IconButton
title="Toggle visibility"
size="small"
sx={{
scale: "0.9",
opacity: 0.5,
":hover": { opacity: 1 },
}}
onClick={() => {
setIsVisible(isVisible === apiKey.id ? "" : apiKey.id);
}}
>
{isVisible === apiKey.id ? (
<VisibilityIcon />
) : (
<VisibilityOffIcon />
)}
</IconButton>
<IconButton
title="Remove API Key"
aria-label="Remove API Key"
size="small"
sx={{
scale: "0.9",
opacity: 0.5,
":hover": { opacity: 1 },
}}
onClick={() => {
setApiKeyToDelete(apiKey);
}}
>
<DeleteIcon />
</IconButton>
</Box>
<IconButton
title="Toggle visibility"
onClick={() => {
setIsVisible(isVisible === apiKey.id ? "" : apiKey.id);
}}
>
{isVisible ? <VisibilityIcon /> : <VisibilityOffIcon />}
</IconButton>
</Box>
</Box>
))}
Expand Down Expand Up @@ -201,6 +239,38 @@ export default function TabAPIKey({ app, environment: env }: Props) {
onSubmit={handleNewKey}
/>
)}
{apiKeyToDelete && (
<ConfirmModal
onCancel={() => {
setApiKeyToDelete(undefined);
}}
onConfirm={({ setLoading, setError }) => {
setLoading(true);
setError(null);
setSuccess(false);

deleteAPIKey(apiKeyToDelete)
.then(() => {
setRefreshToken(Date.now());
})
.catch(() => {
setError("Something went wrong while deleting the API key.");
})
.finally(() => {
setLoading(false);
setApiKeyToDelete(undefined);
});

return "";
}}
>
<Typography>
This will delete the API key.
<br /> If you have any integration that uses this API key, it will
stop working.
</Typography>
</ConfirmModal>
)}
</Box>
);
}
15 changes: 13 additions & 2 deletions src/pages/apps/[id]/environments/[env-id]/config/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,9 +324,14 @@ export const deleteEnvironment = ({
interface FetchAPIKeyProps {
appId: string;
envId: string;
refreshToken?: number;
}

export const useFetchAPIKeys = ({ appId, envId }: FetchAPIKeyProps) => {
export const useFetchAPIKeys = ({
appId,
envId,
refreshToken,
}: FetchAPIKeyProps) => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string>();
const [keys, setKeys] = useState<APIKey[]>([]);
Expand All @@ -346,11 +351,17 @@ export const useFetchAPIKeys = ({ appId, envId }: FetchAPIKeyProps) => {
.finally(() => {
setLoading(false);
});
}, [appId, envId]);
}, [appId, envId, refreshToken]);

return { loading, error, keys, setKeys };
};

export const deleteAPIKey = (apiKey: APIKey) => {
return api.delete<{ keys: APIKey[] }>(
`/app/${apiKey.appId}/env/${apiKey.envId}/api-key?keyId=${apiKey.id}`
);
};

interface GenerateNewAPIKeyProps {
appId: string;
envId?: string;
Expand Down
1 change: 1 addition & 0 deletions src/types/api_key.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ declare interface APIKey {
appId: string;
token: string;
envId?: string;
scope: "env";
}
1 change: 1 addition & 0 deletions testing/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export { default as mockAppResponse } from "./mock_app_response";
export { default as mockUserResponse } from "./mock_user_response";
export { default as mockRemoteConfigResponse } from "./mock_remote_config_response";
export { default as mockAdditionalSettingsResponse } from "./mock_settings_response";
export { default as mockAPIKeysResponse } from "./mock_api_keys_response";
26 changes: 26 additions & 0 deletions testing/data/mock_api_keys_response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
interface APIKeyResponse {
keys: APIKey[];
}

export default (): APIKeyResponse => ({
keys: [
{
appId: "1644802351",
envId: "1429333243019",
id: "8224011755",
scope: "env",
name: "Default",
token:
"SK_fkopNcPhEizN5sV7cZx69tQ1VJZSV6ZiYBdpt6qiojkOB6xbUkCfGbBjR9IxqK",
},
{
appId: "1644802351",
envId: "1429333243019",
id: "9868814106",
scope: "env",
name: "CI",
token:
"SK_m9nvOqzHFXiNrGEzvEQfdJK6isRCLnHpUulb5URvaSMme8fDeNUzPdhzVQLczu",
},
],
});
40 changes: 40 additions & 0 deletions testing/nocks/nock_api_keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import nock from "nock";
import * as data from "../data";

const endpoint = process.env.API_DOMAIN || "";

interface MockFetchAPIKeysProps {
appId: string;
envId: string;
status?: number;
response?: { keys: APIKey[] };
}

export const mockFetchAPIKeys = ({
appId,
envId,
status = 200,
response = data.mockAPIKeysResponse(),
}: MockFetchAPIKeysProps) => {
return nock(endpoint)
.get(`/app/${appId}/env/${envId}/api-keys`)
.reply(status, response);
};

interface MockDeleteAPIKeyProps {
appId: string;
envId: string;
keyId: string;
status?: number;
}

export const mockDeleteAPIKey = ({
appId,
envId,
keyId,
status = 200,
}: MockDeleteAPIKeyProps) => {
return nock(endpoint)
.delete(`/app/${appId}/env/${envId}/api-key?keyId=${keyId}`)
.reply(status, { ok: true });
};

0 comments on commit f87f155

Please sign in to comment.