Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions pkg/app/web/src/__fixtures__/dummy-api-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { APIKeyModel } from "../modules/api-keys";

export const dummyAPIKey = {
id: "api-key-1",
name: "API_KEY_1",
keyHash: "KEY_HASH",
projectId: "pipecd",
role: APIKeyModel.Role.READ_WRITE,
creator: "user",
disabled: false,
createdAt: 0,
updatedAt: 0,
};
46 changes: 46 additions & 0 deletions pkg/app/web/src/api/api-keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { apiClient, apiRequest } from "./client";
import {
GenerateAPIKeyRequest,
GenerateAPIKeyResponse,
DisableAPIKeyRequest,
DisableAPIKeyResponse,
ListAPIKeysRequest,
ListAPIKeysResponse,
} from "pipe/pkg/app/web/api_client/service_pb";
import * as google_protobuf_wrappers_pb from "google-protobuf/google/protobuf/wrappers_pb";

export const getAPIKeys = ({
options,
}: {
options: {
enabled: boolean;
};
}): Promise<ListAPIKeysResponse.AsObject> => {
const req = new ListAPIKeysRequest();
const opt = new ListAPIKeysRequest.Options();
const enabled = new google_protobuf_wrappers_pb.BoolValue();
enabled.setValue(options.enabled);
opt.setEnabled(enabled);
req.setOptions(opt);
return apiRequest(req, apiClient.listAPIKeys);
};

export const generateAPIKey = ({
name,
role,
}: GenerateAPIKeyRequest.AsObject): Promise<
GenerateAPIKeyResponse.AsObject
> => {
const req = new GenerateAPIKeyRequest();
req.setName(name);
req.setRole(role);
return apiRequest(req, apiClient.generateAPIKey);
};

export const disableAPIKey = ({
id,
}: DisableAPIKeyRequest.AsObject): Promise<DisableAPIKeyResponse.AsObject> => {
const req = new DisableAPIKeyRequest();
req.setId(id);
return apiRequest(req, apiClient.disableAPIKey);
};
161 changes: 161 additions & 0 deletions pkg/app/web/src/modules/api-keys.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { dummyAPIKey } from "../__fixtures__/dummy-api-key";
import {
APIKeyModel,
apiKeysSlice,
disableAPIKey,
generateAPIKey,
fetchAPIKeys,
} from "./api-keys";

const baseState = {
error: null,
items: [],
generatedKey: null,
loading: false,
generating: false,
disabling: false,
};

describe("apiKeysSlice reducer", () => {
it("should handle initial state", () => {
expect(
apiKeysSlice.reducer(undefined, {
type: "TEST_ACTION",
})
).toEqual(baseState);
});

describe("generateAPIKey", () => {
const arg = {
name: "new API key",
role: APIKeyModel.Role.READ_ONLY,
};
it(`should handle ${generateAPIKey.pending.type}`, () => {
expect(
apiKeysSlice.reducer(baseState, {
type: generateAPIKey.pending.type,
meta: {
arg,
},
})
).toEqual({ ...baseState, generating: true });
});

it(`should handle ${generateAPIKey.rejected.type}`, () => {
expect(
apiKeysSlice.reducer(
{ ...baseState, generating: true },
{
type: generateAPIKey.rejected.type,
error: { message: "API_ERROR" },
meta: {
arg,
},
}
)
).toEqual({ ...baseState, error: { message: "API_ERROR" } });
});

it(`should handle ${generateAPIKey.fulfilled.type}`, () => {
expect(
apiKeysSlice.reducer(
{ ...baseState, generating: true },
{
type: generateAPIKey.fulfilled.type,
payload: "API_KEY",
meta: { arg },
}
)
).toEqual({ ...baseState, generatedKey: "API_KEY", generating: false });
});
});

describe("fetchAPIKeys", () => {
const arg = {
enabled: true,
};
it(`should handle ${fetchAPIKeys.pending.type}`, () => {
expect(
apiKeysSlice.reducer(baseState, {
type: fetchAPIKeys.pending.type,
meta: {
arg,
},
})
).toEqual({ ...baseState, loading: true });
});

it(`should handle ${fetchAPIKeys.rejected.type}`, () => {
expect(
apiKeysSlice.reducer(
{ ...baseState, loading: true },
{
type: fetchAPIKeys.rejected.type,
error: { message: "API_ERROR" },
meta: {
arg,
},
}
)
).toEqual({ ...baseState, error: { message: "API_ERROR" } });
});

it(`should handle ${fetchAPIKeys.fulfilled.type}`, () => {
expect(
apiKeysSlice.reducer(
{ ...baseState, loading: true },
{
type: fetchAPIKeys.fulfilled.type,
payload: [dummyAPIKey],
meta: { arg },
}
)
).toEqual({ ...baseState, items: [dummyAPIKey] });
});
});

describe("disableAPIKey", () => {
const arg = {
id: "api-key-1",
};

it(`should handle ${disableAPIKey.pending.type}`, () => {
expect(
apiKeysSlice.reducer(baseState, {
type: disableAPIKey.pending.type,
meta: {
arg,
},
})
).toEqual({ ...baseState, disabling: true });
});

it(`should handle ${disableAPIKey.rejected.type}`, () => {
expect(
apiKeysSlice.reducer(
{ ...baseState, disabling: true },
{
type: disableAPIKey.rejected.type,
error: { message: "API_ERROR" },
meta: {
arg,
},
}
)
).toEqual({ ...baseState, error: { message: "API_ERROR" } });
});

it(`should handle ${disableAPIKey.fulfilled.type}`, () => {
expect(
apiKeysSlice.reducer(
{ ...baseState, disabling: true },
{
type: disableAPIKey.fulfilled.type,
payload: [],
meta: { arg },
}
)
).toEqual(baseState);
});
});
});
99 changes: 99 additions & 0 deletions pkg/app/web/src/modules/api-keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {
createAsyncThunk,
createSlice,
SerializedError,
} from "@reduxjs/toolkit";
import { APIKey as APIKeyModel } from "pipe/pkg/app/web/model/apikey_pb";
import * as APIKeysAPI from "../api/api-keys";

const MODULE_NAME = "apiKeys";

export type APIKey = APIKeyModel.AsObject;
interface ApiKeys {
items: APIKey[];
generatedKey: string | null;
loading: boolean;
generating: boolean;
disabling: boolean;
error: null | SerializedError;
}

const initialState: ApiKeys = {
items: [],
generatedKey: null,
loading: false,
generating: false,
disabling: false,
error: null,
};

export const generateAPIKey = createAsyncThunk<
string,
{ name: string; role: APIKeyModel.Role }
>(`${MODULE_NAME}/generate`, async ({ name, role }) => {
const res = await APIKeysAPI.generateAPIKey({ name, role });
return res.key;
});

export const fetchAPIKeys = createAsyncThunk<APIKey[], { enabled: boolean }>(
`${MODULE_NAME}/getList`,
async (options) => {
const res = await APIKeysAPI.getAPIKeys({ options });
return res.keysList;
}
);

export const disableAPIKey = createAsyncThunk<void, { id: string }>(
`${MODULE_NAME}/disable`,
async ({ id }) => {
await APIKeysAPI.disableAPIKey({ id });
}
);

export const apiKeysSlice = createSlice({
name: "apiKeys",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
// generateAPIKey
.addCase(generateAPIKey.pending, (state) => {
state.generating = true;
state.generatedKey = null;
state.error = null;
})
.addCase(generateAPIKey.rejected, (state, action) => {
state.generating = false;
state.error = action.error;
})
.addCase(generateAPIKey.fulfilled, (state, action) => {
state.generating = false;
state.generatedKey = action.payload;
})
// fetchAPIKeys
.addCase(fetchAPIKeys.pending, (state) => {
state.loading = true;
})
.addCase(fetchAPIKeys.rejected, (state, action) => {
state.loading = false;
state.error = action.error;
})
.addCase(fetchAPIKeys.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
})
// disableAPIKey
.addCase(disableAPIKey.pending, (state) => {
state.disabling = true;
})
.addCase(disableAPIKey.rejected, (state, action) => {
state.disabling = false;
state.error = action.error;
})
.addCase(disableAPIKey.fulfilled, (state) => {
state.disabling = false;
});
},
});

export { APIKey as APIKeyModel } from "pipe/pkg/app/web/model/apikey_pb";