diff --git a/src/lib/core/capability.test.ts b/src/lib/core/capability.test.ts index a762d3683..9c4a723e9 100644 --- a/src/lib/core/capability.test.ts +++ b/src/lib/core/capability.test.ts @@ -108,20 +108,22 @@ describe("Capability", () => { expect(mockLog.info).toHaveBeenCalledWith(`Registering schedule store for ${capabilityConfig.name}`); }); - it("should throw an error if store is registered twice", () => { - const capability = new Capability(capabilityConfig); + describe("when a store is registered multiple times", () => { + it("should throw an error if store is registered twice", () => { + const capability = new Capability(capabilityConfig); - capability.registerStore(); - expect(() => capability.registerStore()).toThrowError("Store already registered for test-capability"); - }); + capability.registerStore(); + expect(() => capability.registerStore()).toThrowError("Store already registered for test-capability"); + }); - it("should throw an error if schedule store is registered twice", () => { - const capability = new Capability(capabilityConfig); + it("should throw an error if schedule store is registered twice", () => { + const capability = new Capability(capabilityConfig); - capability.registerScheduleStore(); - expect(() => capability.registerScheduleStore()).toThrowError( - "Schedule store already registered for test-capability", - ); + capability.registerScheduleStore(); + expect(() => capability.registerScheduleStore()).toThrowError( + "Schedule store already registered for test-capability", + ); + }); }); it("should correctly chain When, InNamespace, WithLabel, and Mutate methods", async () => { @@ -159,83 +161,208 @@ describe("Capability", () => { expect(mockLog.info).toHaveBeenCalledWith("Executing mutation action with alias: test-alias"); }); - it("should use child logger for mutate callback", async () => { - const capability = new Capability(capabilityConfig); + describe("when creating log messages", () => { + it("should use child logger for mutate callback", async () => { + const capability = new Capability(capabilityConfig); - const mockMutateCallback: MutateAction = jest.fn( - (req: PeprMutateRequest, logger: typeof Log = mockLog) => { - logger.info("Mutate action log"); - }, - ); + const mockMutateCallback: MutateAction = jest.fn( + (req: PeprMutateRequest, logger: typeof Log = mockLog) => { + logger.info("Mutate action log"); + }, + ); + + capability + .When(a.Pod) + .IsCreatedOrUpdated() + .InNamespace("default") + .WithLabel("test-label", "value") + .Alias("test-alias") + .Mutate(mockMutateCallback); + + expect(capability.bindings).toHaveLength(1); + const binding = capability.bindings[0]; + + // Simulate the mutation action + const peprRequest = new PeprMutateRequest(mockRequest); + + if (binding.mutateCallback) { + await binding.mutateCallback(peprRequest); + } + + expect(mockMutateCallback).toHaveBeenCalledWith(peprRequest, expect.anything()); + expect(mockLog.child).toHaveBeenCalledWith({ alias: "test-alias" }); + expect(mockLog.info).toHaveBeenCalledWith("Executing mutation action with alias: test-alias"); + expect(mockLog.info).toHaveBeenCalledWith("Mutate action log"); + }); - capability - .When(a.Pod) - .IsCreatedOrUpdated() - .InNamespace("default") - .WithLabel("test-label", "value") - .Alias("test-alias") - .Mutate(mockMutateCallback); + it("should handle complex alias and logging correctly", async () => { + const complexCapabilityConfig: CapabilityCfg = { + name: "complex-capability", + description: "Test complex capability description", + namespaces: ["pepr-demo", "pepr-demo-2"], + }; - expect(capability.bindings).toHaveLength(1); - const binding = capability.bindings[0]; + const capability = new Capability(complexCapabilityConfig); - // Simulate the mutation action - const peprRequest = new PeprMutateRequest(mockRequest); + const mockMutateCallback: MutateAction = jest.fn( + async (po: PeprMutateRequest, logger: typeof Log = mockLog) => { + logger.info(`SNAKES ON A PLANE! ${po.Raw.metadata?.name}`); + }, + ); + + capability + .When(a.Pod) + .IsCreatedOrUpdated() + .InNamespace("pepr-demo") + .WithLabel("white") + .Alias("reject:pods:runAsRoot:privileged:runAsGroup<10:allowPrivilegeEscalation") + .Mutate(mockMutateCallback); + + expect(capability.bindings).toHaveLength(1); + const binding = capability.bindings[0]; + expect(binding.filters.namespaces).toContain("pepr-demo"); + expect(binding.filters.labels).toHaveProperty("white", ""); + expect(binding.alias).toBe("reject:pods:runAsRoot:privileged:runAsGroup<10:allowPrivilegeEscalation"); + + // Simulate the mutation action + const peprRequest = new PeprMutateRequest(mockRequest); + + if (binding.mutateCallback) { + await binding.mutateCallback(peprRequest); + } + + expect(mockMutateCallback).toHaveBeenCalledWith(peprRequest, expect.anything()); + expect(mockLog.child).toHaveBeenCalledWith({ + alias: "reject:pods:runAsRoot:privileged:runAsGroup<10:allowPrivilegeEscalation", + }); + expect(mockLog.info).toHaveBeenCalledWith( + "Executing mutation action with alias: reject:pods:runAsRoot:privileged:runAsGroup<10:allowPrivilegeEscalation", + ); + expect(mockLog.info).toHaveBeenCalledWith(`SNAKES ON A PLANE! ${mockRequest.object.metadata?.name}`); + }); - if (binding.mutateCallback) { - await binding.mutateCallback(peprRequest); - } + it("should use child logger for validate callback", async () => { + const capability = new Capability(capabilityConfig); - expect(mockMutateCallback).toHaveBeenCalledWith(peprRequest, expect.anything()); - expect(mockLog.child).toHaveBeenCalledWith({ alias: "test-alias" }); - expect(mockLog.info).toHaveBeenCalledWith("Executing mutation action with alias: test-alias"); - expect(mockLog.info).toHaveBeenCalledWith("Mutate action log"); - }); + const mockValidateCallback: ValidateAction = jest.fn( + async (req: PeprValidateRequest, logger: typeof Log = mockLog) => { + logger.info("Validate action log"); + return { allowed: true }; + }, + ); - it("should handle complex alias and logging correctly", async () => { - const complexCapabilityConfig: CapabilityCfg = { - name: "complex-capability", - description: "Test complex capability description", - namespaces: ["pepr-demo", "pepr-demo-2"], - }; + capability + .When(a.Pod) + .IsCreatedOrUpdated() + .InNamespace("default") + .Alias("test-alias") + .Validate(mockValidateCallback); - const capability = new Capability(complexCapabilityConfig); + expect(capability.bindings).toHaveLength(1); + const binding = capability.bindings[0]; - const mockMutateCallback: MutateAction = jest.fn( - async (po: PeprMutateRequest, logger: typeof Log = mockLog) => { - logger.info(`SNAKES ON A PLANE! ${po.Raw.metadata?.name}`); - }, - ); + // Simulate the validation action + const mockPeprRequest = new PeprValidateRequest(mockRequest); - capability - .When(a.Pod) - .IsCreatedOrUpdated() - .InNamespace("pepr-demo") - .WithLabel("white") - .Alias("reject:pods:runAsRoot:privileged:runAsGroup<10:allowPrivilegeEscalation") - .Mutate(mockMutateCallback); + if (binding.validateCallback) { + await binding.validateCallback(mockPeprRequest); + } - expect(capability.bindings).toHaveLength(1); - const binding = capability.bindings[0]; - expect(binding.filters.namespaces).toContain("pepr-demo"); - expect(binding.filters.labels).toHaveProperty("white", ""); - expect(binding.alias).toBe("reject:pods:runAsRoot:privileged:runAsGroup<10:allowPrivilegeEscalation"); + expect(mockValidateCallback).toHaveBeenCalledWith(mockPeprRequest, expect.anything()); + expect(mockLog.child).toHaveBeenCalledWith({ alias: "test-alias" }); + expect(mockLog.info).toHaveBeenCalledWith("Executing validate action with alias: test-alias"); + expect(mockLog.info).toHaveBeenCalledWith("Validate action log"); + }); - // Simulate the mutation action - const peprRequest = new PeprMutateRequest(mockRequest); + it("should use child logger for reconcile callback", async () => { + const capability = new Capability(capabilityConfig); - if (binding.mutateCallback) { - await binding.mutateCallback(peprRequest); - } + const mockReconcileCallback: WatchLogAction = jest.fn( + async (update, phase, logger: typeof Log = mockLog) => { + logger.info("Reconcile action log"); + }, + ); - expect(mockMutateCallback).toHaveBeenCalledWith(peprRequest, expect.anything()); - expect(mockLog.child).toHaveBeenCalledWith({ - alias: "reject:pods:runAsRoot:privileged:runAsGroup<10:allowPrivilegeEscalation", + capability.When(a.Pod).IsCreatedOrUpdated().Reconcile(mockReconcileCallback); + + expect(capability.bindings).toHaveLength(1); + const binding = capability.bindings[0]; + + // Simulate calling the reconcile action + const testPod = new V1Pod(); + const testPhase = WatchPhase.Modified; + + if (binding.watchCallback) { + await binding.watchCallback(testPod, testPhase); + } + + expect(mockReconcileCallback).toHaveBeenCalledWith(testPod, testPhase, expect.anything()); + expect(mockLog.child).toHaveBeenCalledWith({ alias: "no alias provided" }); + expect(mockLog.info).toHaveBeenCalledWith("Executing reconcile action with alias: no alias provided"); + expect(mockLog.info).toHaveBeenCalledWith("Reconcile action log"); + }); + + it("should use child logger for finalize callback", async () => { + const capability = new Capability(capabilityConfig); + + const mockFinalizeCallback: FinalizeAction = jest.fn( + async (update, logger: typeof Log = mockLog) => { + logger.info("Finalize action log"); + }, + ); + + // Create a mock WatchLogAction function that matches the expected signature + const mockWatchCallback: WatchLogAction = jest.fn( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async (update: V1Pod, phase: WatchPhase, logger?: typeof Log) => {}, + ); + + // Chain .Watch() with the correct function signature before .Finalize() + capability.When(a.Pod).IsCreatedOrUpdated().Watch(mockWatchCallback).Finalize(mockFinalizeCallback); + + // Find the finalize binding + const finalizeBinding = capability.bindings.find(binding => binding.finalizeCallback); + + expect(finalizeBinding).toBeDefined(); // Ensure the finalize binding exists + + // Simulate calling the finalize action + const testPod = new V1Pod(); + + if (finalizeBinding?.finalizeCallback) { + await finalizeBinding.finalizeCallback(testPod); + } + + expect(mockFinalizeCallback).toHaveBeenCalledWith(testPod, expect.anything()); + expect(mockLog.child).toHaveBeenCalledWith({ alias: "no alias provided" }); + expect(mockLog.info).toHaveBeenCalledWith("Executing finalize action with alias: no alias provided"); + expect(mockLog.info).toHaveBeenCalledWith("Finalize action log"); + }); + + it("should use aliasLogger if no logger is provided in watch callback", async () => { + const capability = new Capability(capabilityConfig); + + // Mock the watch callback + const mockWatchCallback: WatchLogAction = jest.fn( + async (update: V1Pod, phase: WatchPhase, logger?: typeof Log) => { + logger?.info("Watch action log"); + }, + ); + + // Chain Watch without providing an explicit logger + capability.When(a.Pod).IsCreatedOrUpdated().Watch(mockWatchCallback); + + expect(capability.bindings).toHaveLength(1); + const binding = capability.bindings[0]; + + // Simulate the watch action without passing a logger, so aliasLogger is used + const testPod = new V1Pod(); + await binding.watchCallback?.(testPod, WatchPhase.Added); // No logger passed + + // Assert that aliasLogger was used + expect(mockLog.child).toHaveBeenCalledWith({ alias: "no alias provided" }); + expect(mockLog.info).toHaveBeenCalledWith("Executing watch action with alias: no alias provided"); + expect(mockLog.info).toHaveBeenCalledWith("Watch action log"); }); - expect(mockLog.info).toHaveBeenCalledWith( - "Executing mutation action with alias: reject:pods:runAsRoot:privileged:runAsGroup<10:allowPrivilegeEscalation", - ); - expect(mockLog.info).toHaveBeenCalledWith(`SNAKES ON A PLANE! ${mockRequest.object.metadata?.name}`); }); it("should reset the alias before each mutation", async () => { @@ -282,39 +409,6 @@ describe("Capability", () => { expect(mockLog.info).toHaveBeenCalledWith("Executing mutation action with alias: no alias provided"); }); - it("should use child logger for validate callback", async () => { - const capability = new Capability(capabilityConfig); - - const mockValidateCallback: ValidateAction = jest.fn( - async (req: PeprValidateRequest, logger: typeof Log = mockLog) => { - logger.info("Validate action log"); - return { allowed: true }; - }, - ); - - capability - .When(a.Pod) - .IsCreatedOrUpdated() - .InNamespace("default") - .Alias("test-alias") - .Validate(mockValidateCallback); - - expect(capability.bindings).toHaveLength(1); - const binding = capability.bindings[0]; - - // Simulate the validation action - const mockPeprRequest = new PeprValidateRequest(mockRequest); - - if (binding.validateCallback) { - await binding.validateCallback(mockPeprRequest); - } - - expect(mockValidateCallback).toHaveBeenCalledWith(mockPeprRequest, expect.anything()); - expect(mockLog.child).toHaveBeenCalledWith({ alias: "test-alias" }); - expect(mockLog.info).toHaveBeenCalledWith("Executing validate action with alias: test-alias"); - expect(mockLog.info).toHaveBeenCalledWith("Validate action log"); - }); - it("should log 'no alias provided' if alias is not set in validate callback", async () => { const capability = new Capability(capabilityConfig); @@ -397,34 +491,6 @@ describe("Capability", () => { expect(mockLog.info).toHaveBeenCalledWith("Watch action executed"); }); - it("should use child logger for reconcile callback", async () => { - const capability = new Capability(capabilityConfig); - - const mockReconcileCallback: WatchLogAction = jest.fn( - async (update, phase, logger: typeof Log = mockLog) => { - logger.info("Reconcile action log"); - }, - ); - - capability.When(a.Pod).IsCreatedOrUpdated().Reconcile(mockReconcileCallback); - - expect(capability.bindings).toHaveLength(1); - const binding = capability.bindings[0]; - - // Simulate calling the reconcile action - const testPod = new V1Pod(); - const testPhase = WatchPhase.Modified; - - if (binding.watchCallback) { - await binding.watchCallback(testPod, testPhase); - } - - expect(mockReconcileCallback).toHaveBeenCalledWith(testPod, testPhase, expect.anything()); - expect(mockLog.child).toHaveBeenCalledWith({ alias: "no alias provided" }); - expect(mockLog.info).toHaveBeenCalledWith("Executing reconcile action with alias: no alias provided"); - expect(mockLog.info).toHaveBeenCalledWith("Reconcile action log"); - }); - it("should use user-provided alias for finalizer with reconcile", async () => { const capability = new Capability(capabilityConfig); @@ -511,122 +577,95 @@ describe("Capability", () => { expect(mockLog.info).toHaveBeenCalledWith("Finalize action log"); }); - it("should use child logger for finalize callback", async () => { - const capability = new Capability(capabilityConfig); - - const mockFinalizeCallback: FinalizeAction = jest.fn(async (update, logger: typeof Log = mockLog) => { - logger.info("Finalize action log"); - }); - - // Create a mock WatchLogAction function that matches the expected signature - const mockWatchCallback: WatchLogAction = jest.fn( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async (update: V1Pod, phase: WatchPhase, logger?: typeof Log) => {}, - ); - - // Chain .Watch() with the correct function signature before .Finalize() - capability.When(a.Pod).IsCreatedOrUpdated().Watch(mockWatchCallback).Finalize(mockFinalizeCallback); - - // Find the finalize binding - const finalizeBinding = capability.bindings.find(binding => binding.finalizeCallback); - - expect(finalizeBinding).toBeDefined(); // Ensure the finalize binding exists - - // Simulate calling the finalize action - const testPod = new V1Pod(); - - if (finalizeBinding?.finalizeCallback) { - await finalizeBinding.finalizeCallback(testPod); - } - - expect(mockFinalizeCallback).toHaveBeenCalledWith(testPod, expect.anything()); - expect(mockLog.child).toHaveBeenCalledWith({ alias: "no alias provided" }); - expect(mockLog.info).toHaveBeenCalledWith("Executing finalize action with alias: no alias provided"); - expect(mockLog.info).toHaveBeenCalledWith("Finalize action log"); - }); - - it("should add deletionTimestamp filter", () => { - const capability = new Capability(capabilityConfig); + describe("when adding filters", () => { + it("should add deletionTimestamp filter", () => { + const capability = new Capability(capabilityConfig); - const mockValidateCallback: ValidateAction = jest.fn( - async (req: PeprValidateRequest, logger: typeof Log = mockLog) => { - logger.info("Validate action log"); - return { allowed: true }; - }, - ); + const mockValidateCallback: ValidateAction = jest.fn( + async (req: PeprValidateRequest, logger: typeof Log = mockLog) => { + logger.info("Validate action log"); + return { allowed: true }; + }, + ); - capability.When(a.Pod).IsCreatedOrUpdated().WithDeletionTimestamp().Validate(mockValidateCallback); + capability.When(a.Pod).IsCreatedOrUpdated().WithDeletionTimestamp().Validate(mockValidateCallback); - expect(capability.bindings).toHaveLength(1); // Ensure binding is created - expect(capability.bindings[0].filters.deletionTimestamp).toBe(true); - }); + expect(capability.bindings).toHaveLength(1); // Ensure binding is created + expect(capability.bindings[0].filters.deletionTimestamp).toBe(true); + }); - it("should add name filter", () => { - const capability = new Capability(capabilityConfig); + it("should add name filter", () => { + const capability = new Capability(capabilityConfig); - const mockValidateCallback: ValidateAction = jest.fn( - async (req: PeprValidateRequest, logger: typeof Log = mockLog) => { - logger.info("Validate action log"); - return { allowed: true }; - }, - ); + const mockValidateCallback: ValidateAction = jest.fn( + async (req: PeprValidateRequest, logger: typeof Log = mockLog) => { + logger.info("Validate action log"); + return { allowed: true }; + }, + ); - capability.When(a.Pod).IsCreatedOrUpdated().WithName("test-name").Validate(mockValidateCallback); + capability.When(a.Pod).IsCreatedOrUpdated().WithName("test-name").Validate(mockValidateCallback); - expect(capability.bindings).toHaveLength(1); // Ensure binding is created - expect(capability.bindings[0].filters.name).toBe("test-name"); - }); + expect(capability.bindings).toHaveLength(1); // Ensure binding is created + expect(capability.bindings[0].filters.name).toBe("test-name"); + }); - it("should add annotation filter", () => { - const capability = new Capability(capabilityConfig); + it("should add annotation filter", () => { + const capability = new Capability(capabilityConfig); - const mockValidateCallback: ValidateAction = jest.fn( - async (req: PeprValidateRequest, logger: typeof Log = mockLog) => { - logger.info("Validate action log"); - return { allowed: true }; - }, - ); + const mockValidateCallback: ValidateAction = jest.fn( + async (req: PeprValidateRequest, logger: typeof Log = mockLog) => { + logger.info("Validate action log"); + return { allowed: true }; + }, + ); - capability.When(a.Pod).IsCreatedOrUpdated().WithAnnotation("test-key", "test-value").Validate(mockValidateCallback); + capability + .When(a.Pod) + .IsCreatedOrUpdated() + .WithAnnotation("test-key", "test-value") + .Validate(mockValidateCallback); - expect(capability.bindings).toHaveLength(1); // Ensure binding is created - expect(capability.bindings[0].filters.annotations["test-key"]).toBe("test-value"); + expect(capability.bindings).toHaveLength(1); // Ensure binding is created + expect(capability.bindings[0].filters.annotations["test-key"]).toBe("test-value"); + }); }); - it("should bind an update event", () => { - const capability = new Capability(capabilityConfig); + describe("when binding to events", () => { + it("should bind an update event", () => { + const capability = new Capability(capabilityConfig); - const mockValidateCallback: ValidateAction = jest.fn( - async (req: PeprValidateRequest, logger: typeof Log = mockLog) => { - logger.info("Validate action log"); - return { allowed: true }; - }, - ); + const mockValidateCallback: ValidateAction = jest.fn( + async (req: PeprValidateRequest, logger: typeof Log = mockLog) => { + logger.info("Validate action log"); + return { allowed: true }; + }, + ); - capability.When(a.Pod).IsUpdated().InNamespace("default").Validate(mockValidateCallback); + capability.When(a.Pod).IsUpdated().InNamespace("default").Validate(mockValidateCallback); - expect(capability.bindings).toHaveLength(1); // Ensure binding is created - expect(capability.bindings[0].event).toBe(Event.UPDATE); - }); + expect(capability.bindings).toHaveLength(1); // Ensure binding is created + expect(capability.bindings[0].event).toBe(Event.UPDATE); + }); - it("should bind a delete event", async () => { - const capability = new Capability(capabilityConfig); + it("should bind a delete event", async () => { + const capability = new Capability(capabilityConfig); - const mockValidateCallback: ValidateAction = jest.fn( - async (req: PeprValidateRequest, logger: typeof Log = mockLog) => { - logger.info("Validate action log"); - return { allowed: true }; - }, - ); + const mockValidateCallback: ValidateAction = jest.fn( + async (req: PeprValidateRequest, logger: typeof Log = mockLog) => { + logger.info("Validate action log"); + return { allowed: true }; + }, + ); - capability.When(a.Pod).IsDeleted().InNamespace("default").Validate(mockValidateCallback); + capability.When(a.Pod).IsDeleted().InNamespace("default").Validate(mockValidateCallback); - expect(capability.bindings).toHaveLength(1); + expect(capability.bindings).toHaveLength(1); - expect(capability.bindings).toHaveLength(1); // Ensure binding is created - expect(capability.bindings[0].event).toBe(Event.DELETE); + expect(capability.bindings).toHaveLength(1); // Ensure binding is created + expect(capability.bindings[0].event).toBe(Event.DELETE); + }); }); - it("should throw an error if neither matchedKind nor kind is provided", () => { const capability = new Capability(capabilityConfig); @@ -701,32 +740,6 @@ describe("Capability", () => { expect(OnSchedule).not.toHaveBeenCalled(); }); - it("should use aliasLogger if no logger is provided in watch callback", async () => { - const capability = new Capability(capabilityConfig); - - // Mock the watch callback - const mockWatchCallback: WatchLogAction = jest.fn( - async (update: V1Pod, phase: WatchPhase, logger?: typeof Log) => { - logger?.info("Watch action log"); - }, - ); - - // Chain Watch without providing an explicit logger - capability.When(a.Pod).IsCreatedOrUpdated().Watch(mockWatchCallback); - - expect(capability.bindings).toHaveLength(1); - const binding = capability.bindings[0]; - - // Simulate the watch action without passing a logger, so aliasLogger is used - const testPod = new V1Pod(); - await binding.watchCallback?.(testPod, WatchPhase.Added); // No logger passed - - // Assert that aliasLogger was used - expect(mockLog.child).toHaveBeenCalledWith({ alias: "no alias provided" }); - expect(mockLog.info).toHaveBeenCalledWith("Executing watch action with alias: no alias provided"); - expect(mockLog.info).toHaveBeenCalledWith("Watch action log"); - }); - it("should add annotation with an empty value when no value is provided in WithAnnotation", () => { const capability = new Capability(capabilityConfig);