Skip to content

Commit

Permalink
chore: add more testing around asset class (#1753)
Browse files Browse the repository at this point in the history
## Description

The assets class does not have unit tests. The purpose of this PR is to
add coverage to ensure the class is working as expected.

Due to the use of dependency injection we are loosely helper functions
with the method and functions on the assets class that rely on members
of the assets class. Due to that, the intent of this PR is to ensure
those injected functions are being with the correct members of the
assets class and to hedge against accidentally change by ensuring
certain functions are called a certain number of times.

## Related Issue

Fixes #1655 
<!-- or -->
Relates to #

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [x] Other (security config, docs update, etc)

## Checklist before merging
- [x] Unit,
[Journey](https://github.com/defenseunicorns/pepr/tree/main/journey),
[E2E Tests](https://github.com/defenseunicorns/pepr-excellent-examples),
[docs](https://github.com/defenseunicorns/pepr/tree/main/docs),
[adr](https://github.com/defenseunicorns/pepr/tree/main/adr) added or
updated as needed
- [x] [Contributor Guide
Steps](https://docs.pepr.dev/main/contribute/#submitting-a-pull-request)
followed

---------

Signed-off-by: Case Wylie <cmwylie19@defenseunicorns.com>
Co-authored-by: Sam Mayer <sam.mayer@defenseunicorns.com>
  • Loading branch information
cmwylie19 and samayer12 authored Feb 4, 2025
1 parent 6b893af commit 90ebb6a
Show file tree
Hide file tree
Showing 3 changed files with 302 additions and 2 deletions.
301 changes: 301 additions & 0 deletions src/lib/assets/assets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
import { ModuleConfig } from "../core/module";
import { Assets } from "./assets";
import { expect, describe, it, jest, afterAll } from "@jest/globals";
import { CapabilityExport } from "../types";
import { kind } from "kubernetes-fluent-client";
import { createDirectoryIfNotExists } from "../filesystemService";
import { promises as fs } from "fs";
import {
V1Deployment,
V1MutatingWebhookConfiguration,
V1Secret,
V1ValidatingWebhookConfiguration,
} from "@kubernetes/client-node/dist/gen";
import { WebhookType } from "../enums";
import { helmLayout } from "./index";

jest.mock("../filesystemService", () => ({
createDirectoryIfNotExists: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
}));
jest.mock("./yaml/overridesFile", () => ({
overridesFile: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
}));
jest.mock("fs", () => ({
...(jest.requireActual("fs") as object),
promises: {
readFile: jest.fn<() => Promise<string>>().mockResolvedValue("mocked"),
writeFile: jest.fn(),
access: jest.fn(),
},
}));

jest.mock("./loader", () => ({
loadCapabilities: jest.fn<() => Promise<CapabilityExport[]>>().mockResolvedValue([
{
name: "capability-1",
description: "A test capability",
namespaces: ["custom-ns", "custom-two"],
bindings: [],
hasSchedule: false,
},
]),
}));

jest.mock("./pods", () => ({
getWatcher: jest.fn<() => V1Deployment>().mockReturnValue({
apiVersion: "apps/v1",
kind: "Deployment",
metadata: {
name: `test-module-watcher`,
namespace: "pepr-system",
},
spec: {
replicas: 1,
selector: {
matchLabels: {
app: "test-module-watcher",
},
},
template: {
metadata: {
labels: {
app: "test-module-watcher",
},
},
spec: {
containers: [
{
name: "test-module-watcher",
image: "test-image",
ports: [
{
containerPort: 8080,
},
],
},
],
},
},
},
}),
getModuleSecret: jest.fn<() => V1Secret>().mockReturnValue({
apiVersion: "v1",
kind: "Secret",
metadata: {
name: `test-module`,
namespace: "pepr-system",
},
type: "Opaque",
data: {
"module-hash.js.gz": "aGVsbG8=",
},
}),
getDeployment: jest.fn<() => V1Deployment>().mockReturnValue({
apiVersion: "apps/v1",
kind: "Deployment",
metadata: {
name: `test-module`,
namespace: "pepr-system",
},
spec: {
replicas: 1,
selector: {
matchLabels: {
app: "test-module",
},
},
template: {
metadata: {
labels: {
app: "test-module",
},
},
spec: {
containers: [
{
name: "test-module",
image: "test-image",
ports: [
{
containerPort: 8080,
},
],
},
],
},
},
},
}),
}));

jest.mock("./index", () => ({
toYaml: jest.fn<() => string>().mockReturnValue("mocked-yaml"),
helmLayout: jest.fn(() => ({
files: {
chartYaml: "/tmp/chart.yaml",
namespaceYaml: "/tmp/namespace.yaml",
watcherServiceYaml: "/tmp/watcher-service.yaml",
admissionServiceYaml: "/tmp/admission-service.yaml",
tlsSecretYaml: "/tmp/tls-secret.yaml",
apiTokenSecretYaml: "/tmp/api-token-secret.yaml",
storeRoleYaml: "/tmp/store-role.yaml",
storeRoleBindingYaml: "/tmp/store-role-binding.yaml",
clusterRoleYaml: "/tmp/cluster-role.yaml",
clusterRoleBindingYaml: "/tmp/cluster-role-binding.yaml",
serviceAccountYaml: "/tmp/service-account.yaml",
moduleSecretYaml: "/tmp/module-secret.yaml",
valuesYaml: "/tmp/values.yaml",
watcherDeploymentYaml: "/tmp/watcher-deployment.yaml",
watcherServiceMonitorYaml: "/tmp/watcher-service-monitor.yaml",
},
dirs: {
templates: "/tmp/templates",
charts: "/tmp/charts",
},
})),
createWebhookYaml: jest.fn(
(
name: string,
config: ModuleConfig,
webhook: kind.MutatingWebhookConfiguration | kind.ValidatingWebhookConfiguration,
) => `mocked-yaml-${name}-${webhook ? "ok" : config.webhookTimeout}`,
),
}));

describe("Assets", () => {
const moduleConfig: ModuleConfig = {
uuid: "test-uuid",
alwaysIgnore: {
namespaces: ["zarf"],
},
peprVersion: "0.0.1",
appVersion: "0.0.1",
description: "A test module",
webhookTimeout: 10,
onError: "reject",
logLevel: "info",
env: {},
rbac: [],
rbacMode: "scoped",
customLabels: {},
};
const assets = new Assets(moduleConfig, "/tmp", ["secret1", "secret2"], "localhost");

afterAll(() => {
jest.clearAllMocks();
});

it("should call deploy function that calls deployFunction with assets, force and webhookTimeout", async () => {
const deployFunction = jest.fn<() => Promise<void>>().mockResolvedValue(undefined);
await assets.deploy(deployFunction, true, assets.config.webhookTimeout);

expect(deployFunction).toHaveBeenCalledWith(assets, true, assets.config.webhookTimeout);
expect(deployFunction).toHaveBeenCalledTimes(1);
});

it("should call zarfYaml that calls zarfYamlGenerator with assets, path, and manifests type", () => {
const zarfYamlGenerator = jest.fn<() => string>().mockReturnValue("");
assets.zarfYaml(zarfYamlGenerator, "/tmp");

expect(zarfYamlGenerator).toHaveBeenCalledWith(assets, "/tmp", "manifests");
expect(zarfYamlGenerator).toHaveBeenCalledTimes(1);
});

it("should call zarfYamlChart that calls zarfYamlGenerator with assets, path, and charts type", () => {
const zarfYamlGenerator = jest.fn<() => string>().mockReturnValue("");
assets.zarfYamlChart(zarfYamlGenerator, "/tmp");

expect(zarfYamlGenerator).toHaveBeenCalledWith(assets, "/tmp", "charts");
expect(zarfYamlGenerator).toHaveBeenCalledTimes(1);
});

it("should call allYaml that calls yamlGenerationFunction with assets deployments", async () => {
const yamlGenerationFunction = jest.fn<() => Promise<string>>().mockResolvedValue("");
await assets.allYaml(yamlGenerationFunction);
const expectedDeployments = {
default: expect.any(Object),
watch: expect.any(Object),
};
expect(yamlGenerationFunction).toHaveBeenCalledTimes(1);
expect(yamlGenerationFunction).toHaveBeenCalledWith(assets, expectedDeployments);
});

it("should call writeWebhookFiles and write admissionController Deployment, ServiceMonitor, and WebhookConfigs", async () => {
const mockHelm = {
files: {
admissionDeploymentYaml: "/tmp/admission-deployment.yaml",
admissionServiceMonitorYaml: "/tmp/admission-service-monitor.yaml",
mutationWebhookYaml: "/tmp/mutation-webhook.yaml",
validationWebhookYaml: "/tmp/validation-webhook.yaml",
},
};
const validateWebhook: V1ValidatingWebhookConfiguration = new kind.ValidatingWebhookConfiguration();
const mutateWebhook: V1MutatingWebhookConfiguration = new kind.MutatingWebhookConfiguration();
await assets.writeWebhookFiles(validateWebhook, mutateWebhook, mockHelm);

expect(fs.writeFile).toHaveBeenCalledTimes(4);
});

it("should call generateHelmChart which should call createDirectoryIfNotExists twice for templates and charts", async () => {
const webhookGeneratorFunction = jest
.fn<() => Promise<V1MutatingWebhookConfiguration | V1ValidatingWebhookConfiguration | null>>()
.mockResolvedValue(new kind.MutatingWebhookConfiguration());
await assets.generateHelmChart(webhookGeneratorFunction, "/tmp");
expect(createDirectoryIfNotExists).toHaveBeenCalledTimes(2);
});

it("should call generateHelmChart which should write file 40 times for built Kubernetes Manifests and helm chart generation", async () => {
const webhookGeneratorFunction = jest
.fn<() => Promise<V1MutatingWebhookConfiguration | V1ValidatingWebhookConfiguration | null>>()
.mockResolvedValue(new kind.MutatingWebhookConfiguration());
await assets.generateHelmChart(webhookGeneratorFunction, "/tmp");
expect(fs.writeFile).toHaveBeenCalledTimes(40);
});

it("should call generateHelmChart and get no error", async () => {
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {});

const webhookGeneratorFunction = jest
.fn<() => Promise<V1MutatingWebhookConfiguration | V1ValidatingWebhookConfiguration | null>>()
.mockResolvedValue(new kind.MutatingWebhookConfiguration());
await assets.generateHelmChart(webhookGeneratorFunction, "/tmp");
expect(consoleErrorSpy).not.toHaveBeenCalled();
});

it("should call generateHelmChart without an error when asset class instance is correct", async () => {
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {});

const webhookGeneratorFunction = jest
.fn<() => Promise<V1MutatingWebhookConfiguration | V1ValidatingWebhookConfiguration | null>>()
.mockResolvedValue(new kind.MutatingWebhookConfiguration());
await assets.generateHelmChart(webhookGeneratorFunction, "/tmp");
expect(consoleErrorSpy).not.toHaveBeenCalled();
});

it("should call generateHelmChart and throw an error when config is incorrect", async () => {
const exitString = "Mock console.exit call";
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
const processExitSpy = jest.spyOn(process, "exit").mockImplementation(() => {
throw new Error(exitString);
});

(helmLayout as jest.Mock).mockReturnValue(null);

const webhookGeneratorFunction = jest
.fn<
(
assets: Assets,
mutateOrValidate: WebhookType,
timeoutSeconds: number | undefined,
) => Promise<V1MutatingWebhookConfiguration | V1ValidatingWebhookConfiguration | null>
>()
.mockResolvedValue(new kind.ValidatingWebhookConfiguration());

await expect(assets.generateHelmChart(webhookGeneratorFunction, "/tmp")).rejects.toThrow(exitString);

expect(consoleErrorSpy).toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(1);
});
});
2 changes: 1 addition & 1 deletion src/lib/assets/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export class Assets {

allYaml = async (
yamlGenerationFunction: (
assyts: Assets,
assets: Assets,
deployments: { default: V1Deployment; watch: V1Deployment | null },
) => Promise<string>,
imagePullSecret?: string,
Expand Down
1 change: 0 additions & 1 deletion src/lib/assets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export function toYaml(obj: any): string {
return dumpYaml(obj, { noRefs: true });
}

// Unit Test Me!!
export function createWebhookYaml(
name: string,
config: ModuleConfig,
Expand Down

0 comments on commit 90ebb6a

Please sign in to comment.