mirror of https://github.com/portainer/portainer
				
				
				
			fix(app/kubernetes): Fix listing of secrets and configmaps with same name [r8s-288] (#897)
							parent
							
								
									383bcc4113
								
							
						
					
					
						commit
						4e4c5ffdb6
					
				| 
						 | 
				
			
			@ -0,0 +1,702 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import { render, screen } from '@testing-library/react';
 | 
			
		||||
import { vi, beforeEach, afterEach } from 'vitest';
 | 
			
		||||
 | 
			
		||||
import { Application } from '../../types';
 | 
			
		||||
 | 
			
		||||
import { ApplicationEnvVarsTable } from './ApplicationEnvVarsTable';
 | 
			
		||||
 | 
			
		||||
// Mock icon components
 | 
			
		||||
vi.mock('lucide-react', () => ({
 | 
			
		||||
  Asterisk: () => <span data-cy="asterisk-icon" />,
 | 
			
		||||
  File: () => <span data-cy="file-icon" />,
 | 
			
		||||
  FileCode: () => <span data-cy="file-code-icon" />,
 | 
			
		||||
  Key: () => <span data-cy="key-icon" />,
 | 
			
		||||
  Lock: () => <span data-cy="lock-icon" />,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
// Mock UI components
 | 
			
		||||
vi.mock('@@/Icon', () => ({
 | 
			
		||||
  Icon: ({
 | 
			
		||||
    icon: IconComponent,
 | 
			
		||||
    ...props
 | 
			
		||||
  }: {
 | 
			
		||||
    icon: React.ComponentType;
 | 
			
		||||
    [key: string]: unknown;
 | 
			
		||||
  }) => <IconComponent {...props} />,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
vi.mock('@@/Tip/TextTip', () => ({
 | 
			
		||||
  TextTip: ({
 | 
			
		||||
    children,
 | 
			
		||||
    color,
 | 
			
		||||
  }: {
 | 
			
		||||
    children: React.ReactNode;
 | 
			
		||||
    color?: string;
 | 
			
		||||
  }) => (
 | 
			
		||||
    <div data-cy="text-tip" data-color={color}>
 | 
			
		||||
      {children}
 | 
			
		||||
    </div>
 | 
			
		||||
  ),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
// Mock the Link component to capture routing props
 | 
			
		||||
const mockLink = vi.fn();
 | 
			
		||||
vi.mock('@@/Link', () => ({
 | 
			
		||||
  Link: ({
 | 
			
		||||
    children,
 | 
			
		||||
    to,
 | 
			
		||||
    params,
 | 
			
		||||
    'data-cy': dataCy,
 | 
			
		||||
    className,
 | 
			
		||||
  }: {
 | 
			
		||||
    children: React.ReactNode;
 | 
			
		||||
    to: string;
 | 
			
		||||
    params: Record<string, string>;
 | 
			
		||||
    'data-cy'?: string;
 | 
			
		||||
    className?: string;
 | 
			
		||||
  }) => {
 | 
			
		||||
    mockLink({ children, to, params, 'data-cy': dataCy, className });
 | 
			
		||||
    return (
 | 
			
		||||
      <span
 | 
			
		||||
        data-cy={dataCy}
 | 
			
		||||
        data-testid={dataCy}
 | 
			
		||||
        role="link"
 | 
			
		||||
        data-to={to}
 | 
			
		||||
        data-params={JSON.stringify(params)}
 | 
			
		||||
        className={className}
 | 
			
		||||
      >
 | 
			
		||||
        {children}
 | 
			
		||||
      </span>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
describe('ApplicationEnvVarsTable', () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    mockLink.mockClear();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  afterEach(() => {
 | 
			
		||||
    vi.clearAllMocks();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render helpful tip when there are no environment variables', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'test-container',
 | 
			
		||||
            image: 'test-image',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationEnvVarsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    expect(
 | 
			
		||||
      screen.getByText('Environment variables, ConfigMaps or Secrets')
 | 
			
		||||
    ).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByTestId('text-tip')).toBeInTheDocument();
 | 
			
		||||
    expect(
 | 
			
		||||
      screen.getByText(
 | 
			
		||||
        'This application is not using any environment variable, ConfigMap or Secret.'
 | 
			
		||||
      )
 | 
			
		||||
    ).toBeInTheDocument();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render nothing when app is undefined', () => {
 | 
			
		||||
    render(<ApplicationEnvVarsTable namespace="default" app={undefined} />);
 | 
			
		||||
 | 
			
		||||
    expect(
 | 
			
		||||
      screen.getByText('Environment variables, ConfigMaps or Secrets')
 | 
			
		||||
    ).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByTestId('text-tip')).toBeInTheDocument();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render regular environment variables with direct values', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'test-container',
 | 
			
		||||
            image: 'test-image',
 | 
			
		||||
            env: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'ENV_VAR',
 | 
			
		||||
                value: 'test-value',
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationEnvVarsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    expect(screen.getByText('test-container')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('ENV_VAR')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('test-value')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('-')).toBeInTheDocument(); // No configuration resource
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render configmap environment variables with correct routing', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'test-container',
 | 
			
		||||
            image: 'test-image',
 | 
			
		||||
            env: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'CONFIG_VAR',
 | 
			
		||||
                valueFrom: {
 | 
			
		||||
                  configMapKeyRef: {
 | 
			
		||||
                    name: 'test-configmap',
 | 
			
		||||
                    key: 'config-key',
 | 
			
		||||
                  },
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationEnvVarsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    expect(screen.getByText('test-container')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getAllByText('CONFIG_VAR')).toHaveLength(2); // Appears in name and value columns
 | 
			
		||||
    // Note: config-key is not displayed in UI - the component shows the env var name
 | 
			
		||||
    expect(screen.getByText('test-configmap')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByTestId('key-icon')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByTestId('file-code-icon')).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
    // Verify the Link component was called with correct routing parameters
 | 
			
		||||
    expect(mockLink).toHaveBeenCalledWith(
 | 
			
		||||
      expect.objectContaining({
 | 
			
		||||
        to: 'kubernetes.configmaps.configmap',
 | 
			
		||||
        params: {
 | 
			
		||||
          name: 'test-configmap',
 | 
			
		||||
          namespace: 'default',
 | 
			
		||||
        },
 | 
			
		||||
        'data-cy': 'configmap-link-test-configmap',
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render secret environment variables with correct routing', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'test-container',
 | 
			
		||||
            image: 'test-image',
 | 
			
		||||
            env: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'SECRET_VAR',
 | 
			
		||||
                valueFrom: {
 | 
			
		||||
                  secretKeyRef: {
 | 
			
		||||
                    name: 'test-secret',
 | 
			
		||||
                    key: 'secret-key',
 | 
			
		||||
                  },
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationEnvVarsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    expect(screen.getByText('test-container')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getAllByText('SECRET_VAR')).toHaveLength(2); // Appears in name and value columns
 | 
			
		||||
    // Note: secret-key is not displayed in UI - the component shows the env var name
 | 
			
		||||
    expect(screen.getByText('test-secret')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByTestId('key-icon')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByTestId('lock-icon')).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
    // Verify the Link component was called with correct routing parameters for secret
 | 
			
		||||
    expect(mockLink).toHaveBeenCalledWith(
 | 
			
		||||
      expect.objectContaining({
 | 
			
		||||
        to: 'kubernetes.secrets.secret',
 | 
			
		||||
        params: {
 | 
			
		||||
          name: 'test-secret',
 | 
			
		||||
          namespace: 'default',
 | 
			
		||||
        },
 | 
			
		||||
        'data-cy': 'configmap-link-test-secret',
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render downward API field references', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'test-container',
 | 
			
		||||
            image: 'test-image',
 | 
			
		||||
            env: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'POD_NAME',
 | 
			
		||||
                valueFrom: {
 | 
			
		||||
                  fieldRef: {
 | 
			
		||||
                    fieldPath: 'metadata.name',
 | 
			
		||||
                  },
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationEnvVarsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    expect(screen.getByText('test-container')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('POD_NAME')).toBeInTheDocument();
 | 
			
		||||
    expect(
 | 
			
		||||
      screen.getByText(
 | 
			
		||||
        (content, element) =>
 | 
			
		||||
          content.includes('metadata.name') && element?.tagName === 'SPAN'
 | 
			
		||||
      )
 | 
			
		||||
    ).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('downward API')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByTestId('asterisk-icon')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('-')).toBeInTheDocument(); // No configuration resource
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render envFrom configmap (entire configmap import)', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'test-container',
 | 
			
		||||
            image: 'test-image',
 | 
			
		||||
            envFrom: [
 | 
			
		||||
              {
 | 
			
		||||
                configMapRef: {
 | 
			
		||||
                  name: 'entire-configmap',
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationEnvVarsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    expect(screen.getByText('test-container')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getAllByText('-')).toHaveLength(2); // EnvFrom doesn't have a specific key name or value
 | 
			
		||||
    expect(screen.getByText('entire-configmap')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByTestId('file-code-icon')).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
    // Verify configmap routing
 | 
			
		||||
    expect(mockLink).toHaveBeenCalledWith(
 | 
			
		||||
      expect.objectContaining({
 | 
			
		||||
        to: 'kubernetes.configmaps.configmap',
 | 
			
		||||
        params: {
 | 
			
		||||
          name: 'entire-configmap',
 | 
			
		||||
          namespace: 'default',
 | 
			
		||||
        },
 | 
			
		||||
        'data-cy': 'configmap-link-entire-configmap',
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render envFrom secret (entire secret import)', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'test-container',
 | 
			
		||||
            image: 'test-image',
 | 
			
		||||
            envFrom: [
 | 
			
		||||
              {
 | 
			
		||||
                secretRef: {
 | 
			
		||||
                  name: 'entire-secret',
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationEnvVarsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    expect(screen.getByText('test-container')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getAllByText('-')).toHaveLength(2); // EnvFrom doesn't have a specific key name or value
 | 
			
		||||
    expect(screen.getByText('entire-secret')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByTestId('lock-icon')).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
    // Verify secret routing
 | 
			
		||||
    expect(mockLink).toHaveBeenCalledWith(
 | 
			
		||||
      expect.objectContaining({
 | 
			
		||||
        to: 'kubernetes.secrets.secret',
 | 
			
		||||
        params: {
 | 
			
		||||
          name: 'entire-secret',
 | 
			
		||||
          namespace: 'default',
 | 
			
		||||
        },
 | 
			
		||||
        'data-cy': 'configmap-link-entire-secret',
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render init containers with asterisk indicator', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'main-container',
 | 
			
		||||
            image: 'main-image',
 | 
			
		||||
            env: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'MAIN_VAR',
 | 
			
		||||
                value: 'main-value',
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        initContainers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'init-container',
 | 
			
		||||
            image: 'init-image',
 | 
			
		||||
            env: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'INIT_VAR',
 | 
			
		||||
                value: 'init-value',
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationEnvVarsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    // Check main container
 | 
			
		||||
    expect(screen.getByText('main-container')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('MAIN_VAR')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('main-value')).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
    // Check init container
 | 
			
		||||
    expect(screen.getByText('init-container')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('INIT_VAR')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('init-value')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('init container')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByTestId('asterisk-icon')).toBeInTheDocument();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should handle mixed environment variable types correctly', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'test-container',
 | 
			
		||||
            image: 'test-image',
 | 
			
		||||
            env: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'REGULAR_VAR',
 | 
			
		||||
                value: 'regular-value',
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                name: 'CONFIG_VAR',
 | 
			
		||||
                valueFrom: {
 | 
			
		||||
                  configMapKeyRef: {
 | 
			
		||||
                    name: 'test-configmap',
 | 
			
		||||
                    key: 'config-key',
 | 
			
		||||
                  },
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                name: 'SECRET_VAR',
 | 
			
		||||
                valueFrom: {
 | 
			
		||||
                  secretKeyRef: {
 | 
			
		||||
                    name: 'test-secret',
 | 
			
		||||
                    key: 'secret-key',
 | 
			
		||||
                  },
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
            envFrom: [
 | 
			
		||||
              {
 | 
			
		||||
                configMapRef: {
 | 
			
		||||
                  name: 'entire-configmap',
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationEnvVarsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    expect(screen.getAllByText('test-container')).toHaveLength(4); // Should appear 4 times - once for each env var
 | 
			
		||||
    expect(screen.getByText('REGULAR_VAR')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('regular-value')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getAllByText('CONFIG_VAR')).toHaveLength(2); // Appears in name and value columns
 | 
			
		||||
    // Note: config-key is not displayed in UI - the component shows the env var name
 | 
			
		||||
    expect(screen.getAllByText('SECRET_VAR')).toHaveLength(2); // Appears in name and value columns
 | 
			
		||||
    // Note: secret-key is not displayed in UI - the component shows the env var name
 | 
			
		||||
    expect(screen.getByText('test-configmap')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('test-secret')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('entire-configmap')).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
    // Should have made multiple Link calls
 | 
			
		||||
    expect(mockLink).toHaveBeenCalledTimes(3);
 | 
			
		||||
 | 
			
		||||
    // Verify different routing calls
 | 
			
		||||
    expect(mockLink).toHaveBeenCalledWith(
 | 
			
		||||
      expect.objectContaining({
 | 
			
		||||
        to: 'kubernetes.configmaps.configmap',
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    expect(mockLink).toHaveBeenCalledWith(
 | 
			
		||||
      expect.objectContaining({
 | 
			
		||||
        to: 'kubernetes.secrets.secret',
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should handle Deployment kind applications', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-deployment', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        selector: {
 | 
			
		||||
          matchLabels: {
 | 
			
		||||
            app: 'test-app',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        template: {
 | 
			
		||||
          spec: {
 | 
			
		||||
            containers: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'test-container',
 | 
			
		||||
                image: 'test-image',
 | 
			
		||||
                env: [
 | 
			
		||||
                  {
 | 
			
		||||
                    name: 'ENV_VAR',
 | 
			
		||||
                    value: 'test-value',
 | 
			
		||||
                  },
 | 
			
		||||
                ],
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Deployment',
 | 
			
		||||
      apiVersion: 'apps/v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationEnvVarsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    expect(screen.getByText('test-container')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('ENV_VAR')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('test-value')).toBeInTheDocument();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should handle missing resource names gracefully', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'test-container',
 | 
			
		||||
            image: 'test-image',
 | 
			
		||||
            env: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'CONFIG_VAR',
 | 
			
		||||
                valueFrom: {
 | 
			
		||||
                  configMapKeyRef: {
 | 
			
		||||
                    // name is undefined
 | 
			
		||||
                    key: 'config-key',
 | 
			
		||||
                  },
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationEnvVarsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    expect(screen.getByText('test-container')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getAllByText('CONFIG_VAR')).toHaveLength(2); // Appears in name and value columns
 | 
			
		||||
    // Note: config-key is not displayed in UI - the component shows the env var name
 | 
			
		||||
 | 
			
		||||
    // Should show dash for missing resource name
 | 
			
		||||
    const dashElements = screen.getAllByText('-');
 | 
			
		||||
    expect(dashElements.length).toBeGreaterThan(0);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should handle containers without environment variables', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'test-container',
 | 
			
		||||
            image: 'test-image',
 | 
			
		||||
            // No env or envFrom
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationEnvVarsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    expect(
 | 
			
		||||
      screen.getByText('Environment variables, ConfigMaps or Secrets')
 | 
			
		||||
    ).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByTestId('text-tip')).toBeInTheDocument();
 | 
			
		||||
    expect(
 | 
			
		||||
      screen.getByText(
 | 
			
		||||
        'This application is not using any environment variable, ConfigMap or Secret.'
 | 
			
		||||
      )
 | 
			
		||||
    ).toBeInTheDocument();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should handle environment variables without keys', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'test-container',
 | 
			
		||||
            image: 'test-image',
 | 
			
		||||
            env: [
 | 
			
		||||
              {
 | 
			
		||||
                name: '', // Empty name to test this edge case
 | 
			
		||||
                value: 'test-value',
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationEnvVarsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    expect(screen.getByText('test-container')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('test-value')).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
    // Should show dash for missing env var name
 | 
			
		||||
    const dashElements = screen.getAllByText('-');
 | 
			
		||||
    expect(dashElements.length).toBeGreaterThan(0);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render multiple containers with different environment variable types', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'container-1',
 | 
			
		||||
            image: 'image-1',
 | 
			
		||||
            env: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'CONFIG_VAR',
 | 
			
		||||
                valueFrom: {
 | 
			
		||||
                  configMapKeyRef: {
 | 
			
		||||
                    name: 'shared-config',
 | 
			
		||||
                    key: 'config-key',
 | 
			
		||||
                  },
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'container-2',
 | 
			
		||||
            image: 'image-2',
 | 
			
		||||
            env: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'SECRET_VAR',
 | 
			
		||||
                valueFrom: {
 | 
			
		||||
                  secretKeyRef: {
 | 
			
		||||
                    name: 'shared-config', // Same name but different type
 | 
			
		||||
                    key: 'secret-key',
 | 
			
		||||
                  },
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationEnvVarsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    expect(screen.getByText('container-1')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('container-2')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getAllByText('CONFIG_VAR')).toHaveLength(2); // Appears in name and value columns
 | 
			
		||||
    expect(screen.getAllByText('SECRET_VAR')).toHaveLength(2); // Appears in name and value columns
 | 
			
		||||
    expect(screen.getAllByText('shared-config')).toHaveLength(2);
 | 
			
		||||
 | 
			
		||||
    // Should have made two Link calls - one for configmap, one for secret
 | 
			
		||||
    expect(mockLink).toHaveBeenCalledTimes(2);
 | 
			
		||||
 | 
			
		||||
    // Verify configmap routing
 | 
			
		||||
    expect(mockLink).toHaveBeenCalledWith(
 | 
			
		||||
      expect.objectContaining({
 | 
			
		||||
        to: 'kubernetes.configmaps.configmap',
 | 
			
		||||
        params: {
 | 
			
		||||
          name: 'shared-config',
 | 
			
		||||
          namespace: 'default',
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Verify secret routing
 | 
			
		||||
    expect(mockLink).toHaveBeenCalledWith(
 | 
			
		||||
      expect.objectContaining({
 | 
			
		||||
        to: 'kubernetes.secrets.secret',
 | 
			
		||||
        params: {
 | 
			
		||||
          name: 'shared-config',
 | 
			
		||||
          namespace: 'default',
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -84,8 +84,8 @@ export function ApplicationEnvVarsTable({ namespace, app }: Props) {
 | 
			
		|||
                    ))}
 | 
			
		||||
                </td>
 | 
			
		||||
                <td data-cy="k8sAppDetail-configName">
 | 
			
		||||
                  {!envVar.resourseName && <span>-</span>}
 | 
			
		||||
                  {envVar.resourseName && (
 | 
			
		||||
                  {!envVar.resourceName && <span>-</span>}
 | 
			
		||||
                  {envVar.resourceName && (
 | 
			
		||||
                    <span>
 | 
			
		||||
                      <Link
 | 
			
		||||
                        to={
 | 
			
		||||
| 
						 | 
				
			
			@ -94,17 +94,17 @@ export function ApplicationEnvVarsTable({ namespace, app }: Props) {
 | 
			
		|||
                            : 'kubernetes.secrets.secret'
 | 
			
		||||
                        }
 | 
			
		||||
                        params={{
 | 
			
		||||
                          name: envVar.resourseName,
 | 
			
		||||
                          name: envVar.resourceName,
 | 
			
		||||
                          namespace,
 | 
			
		||||
                        }}
 | 
			
		||||
                        className="flex items-center"
 | 
			
		||||
                        data-cy={`configmap-link-${envVar.resourseName}`}
 | 
			
		||||
                        data-cy={`configmap-link-${envVar.resourceName}`}
 | 
			
		||||
                      >
 | 
			
		||||
                        <Icon
 | 
			
		||||
                          icon={envVar.type === 'configMap' ? FileCode : Lock}
 | 
			
		||||
                          className="!mr-1"
 | 
			
		||||
                        />
 | 
			
		||||
                        {envVar.resourseName}
 | 
			
		||||
                        {envVar.resourceName}
 | 
			
		||||
                      </Link>
 | 
			
		||||
                    </span>
 | 
			
		||||
                  )}
 | 
			
		||||
| 
						 | 
				
			
			@ -126,7 +126,7 @@ interface ContainerEnvVar {
 | 
			
		|||
  containerName: string;
 | 
			
		||||
  isInitContainer: boolean;
 | 
			
		||||
  type: EnvVarType;
 | 
			
		||||
  resourseName: string;
 | 
			
		||||
  resourceName: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getApplicationEnvironmentVariables(
 | 
			
		||||
| 
						 | 
				
			
			@ -159,7 +159,7 @@ function getApplicationEnvironmentVariables(
 | 
			
		|||
            containerName: container.name,
 | 
			
		||||
            isInitContainer: false,
 | 
			
		||||
            type: envtype,
 | 
			
		||||
            resourseName:
 | 
			
		||||
            resourceName:
 | 
			
		||||
              envVar?.valueFrom?.configMapKeyRef?.name ||
 | 
			
		||||
              envVar?.valueFrom?.secretKeyRef?.name ||
 | 
			
		||||
              '',
 | 
			
		||||
| 
						 | 
				
			
			@ -170,7 +170,7 @@ function getApplicationEnvironmentVariables(
 | 
			
		|||
      const containerEnvFroms: ContainerEnvVar[] =
 | 
			
		||||
        container?.envFrom?.map((envFrom) => ({
 | 
			
		||||
          name: '',
 | 
			
		||||
          resourseName:
 | 
			
		||||
          resourceName:
 | 
			
		||||
            envFrom?.configMapRef?.name || envFrom?.secretRef?.name || '',
 | 
			
		||||
          containerName: container.name,
 | 
			
		||||
          isInitContainer: false,
 | 
			
		||||
| 
						 | 
				
			
			@ -196,7 +196,7 @@ function getApplicationEnvironmentVariables(
 | 
			
		|||
            containerName: container.name,
 | 
			
		||||
            isInitContainer: true,
 | 
			
		||||
            type: envtype,
 | 
			
		||||
            resourseName:
 | 
			
		||||
            resourceName:
 | 
			
		||||
              envVar?.valueFrom?.configMapKeyRef?.name ||
 | 
			
		||||
              envVar?.valueFrom?.secretKeyRef?.name ||
 | 
			
		||||
              '',
 | 
			
		||||
| 
						 | 
				
			
			@ -207,7 +207,7 @@ function getApplicationEnvironmentVariables(
 | 
			
		|||
      const containerEnvFroms: ContainerEnvVar[] =
 | 
			
		||||
        container?.envFrom?.map((envFrom) => ({
 | 
			
		||||
          name: '',
 | 
			
		||||
          resourseName:
 | 
			
		||||
          resourceName:
 | 
			
		||||
            envFrom?.configMapRef?.name || envFrom?.secretRef?.name || '',
 | 
			
		||||
          containerName: container.name,
 | 
			
		||||
          isInitContainer: true,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,583 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import { render, screen } from '@testing-library/react';
 | 
			
		||||
import { vi, beforeEach, afterEach } from 'vitest';
 | 
			
		||||
 | 
			
		||||
import { Application } from '../../types';
 | 
			
		||||
 | 
			
		||||
import { ApplicationVolumeConfigsTable } from './ApplicationVolumeConfigsTable';
 | 
			
		||||
 | 
			
		||||
// Mock icon components
 | 
			
		||||
vi.mock('lucide-react', () => ({
 | 
			
		||||
  Asterisk: () => <span data-cy="asterisk-icon" />,
 | 
			
		||||
  Plus: () => <span data-cy="plus-icon" />,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
// Mock UI components
 | 
			
		||||
vi.mock('@@/Icon', () => ({
 | 
			
		||||
  Icon: ({
 | 
			
		||||
    icon: IconComponent,
 | 
			
		||||
    ...props
 | 
			
		||||
  }: {
 | 
			
		||||
    icon: React.ComponentType;
 | 
			
		||||
    [key: string]: unknown;
 | 
			
		||||
  }) => <IconComponent {...props} />,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
// Mock the Link component to capture routing props
 | 
			
		||||
const mockLink = vi.fn();
 | 
			
		||||
vi.mock('@@/Link', () => ({
 | 
			
		||||
  Link: ({
 | 
			
		||||
    children,
 | 
			
		||||
    to,
 | 
			
		||||
    params,
 | 
			
		||||
    'data-cy': dataCy,
 | 
			
		||||
    className,
 | 
			
		||||
  }: {
 | 
			
		||||
    children: React.ReactNode;
 | 
			
		||||
    to: string;
 | 
			
		||||
    params: Record<string, string>;
 | 
			
		||||
    'data-cy'?: string;
 | 
			
		||||
    className?: string;
 | 
			
		||||
  }) => {
 | 
			
		||||
    mockLink({ children, to, params, 'data-cy': dataCy, className });
 | 
			
		||||
    return (
 | 
			
		||||
      <span
 | 
			
		||||
        data-cy={dataCy}
 | 
			
		||||
        data-testid={dataCy}
 | 
			
		||||
        role="link"
 | 
			
		||||
        data-to={to}
 | 
			
		||||
        data-params={JSON.stringify(params)}
 | 
			
		||||
        className={className}
 | 
			
		||||
      >
 | 
			
		||||
        {children}
 | 
			
		||||
      </span>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
describe('ApplicationVolumeConfigsTable', () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    mockLink.mockClear();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  afterEach(() => {
 | 
			
		||||
    vi.clearAllMocks();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render nothing when there are no volume configurations', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'test-container',
 | 
			
		||||
            image: 'test-image',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const { container } = render(
 | 
			
		||||
      <ApplicationVolumeConfigsTable namespace="default" app={app} />
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    expect(container.firstChild).toBeNull();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render nothing when app is undefined', () => {
 | 
			
		||||
    const { container } = render(
 | 
			
		||||
      <ApplicationVolumeConfigsTable namespace="default" app={undefined} />
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    expect(container.firstChild).toBeNull();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render volume configurations from configmaps with items', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'test-container',
 | 
			
		||||
            image: 'test-image',
 | 
			
		||||
            volumeMounts: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'config-volume',
 | 
			
		||||
                mountPath: '/etc/config',
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        volumes: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'config-volume',
 | 
			
		||||
            configMap: {
 | 
			
		||||
              name: 'test-configmap',
 | 
			
		||||
              items: [
 | 
			
		||||
                {
 | 
			
		||||
                  key: 'config-key',
 | 
			
		||||
                  path: 'config.yaml',
 | 
			
		||||
                },
 | 
			
		||||
              ],
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationVolumeConfigsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    expect(screen.getByText('test-container')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('/etc/config/config.yaml')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('config-key')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getAllByTestId('plus-icon')).toHaveLength(2); // One for value, one for link
 | 
			
		||||
    expect(screen.getByText('test-configmap')).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
    // Verify the Link component was called with correct routing parameters
 | 
			
		||||
    expect(mockLink).toHaveBeenCalledWith(
 | 
			
		||||
      expect.objectContaining({
 | 
			
		||||
        to: 'kubernetes.configmaps.configmap',
 | 
			
		||||
        params: {
 | 
			
		||||
          name: 'test-configmap',
 | 
			
		||||
          namespace: 'default',
 | 
			
		||||
        },
 | 
			
		||||
        'data-cy': 'config-link-test-configmap',
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render volume configurations from secrets with items', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'test-container',
 | 
			
		||||
            image: 'test-image',
 | 
			
		||||
            volumeMounts: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'secret-volume',
 | 
			
		||||
                mountPath: '/etc/secrets',
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        volumes: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'secret-volume',
 | 
			
		||||
            secret: {
 | 
			
		||||
              secretName: 'test-secret',
 | 
			
		||||
              items: [
 | 
			
		||||
                {
 | 
			
		||||
                  key: 'secret-key',
 | 
			
		||||
                  path: 'secret.txt',
 | 
			
		||||
                },
 | 
			
		||||
              ],
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationVolumeConfigsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    expect(screen.getByText('test-container')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('/etc/secrets/secret.txt')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('secret-key')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getAllByTestId('plus-icon')).toHaveLength(2); // One for value, one for link
 | 
			
		||||
    expect(screen.getByText('test-secret')).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
    // Verify the Link component was called with correct routing parameters for secret
 | 
			
		||||
    expect(mockLink).toHaveBeenCalledWith(
 | 
			
		||||
      expect.objectContaining({
 | 
			
		||||
        to: 'kubernetes.secrets.secret',
 | 
			
		||||
        params: {
 | 
			
		||||
          name: 'test-secret',
 | 
			
		||||
          namespace: 'default',
 | 
			
		||||
        },
 | 
			
		||||
        'data-cy': 'secret-link-test-secret',
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render init containers with asterisk indicator', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'main-container',
 | 
			
		||||
            image: 'main-image',
 | 
			
		||||
            volumeMounts: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'config-volume',
 | 
			
		||||
                mountPath: '/etc/config',
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        initContainers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'init-container',
 | 
			
		||||
            image: 'init-image',
 | 
			
		||||
            volumeMounts: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'config-volume',
 | 
			
		||||
                mountPath: '/etc/init-config',
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        volumes: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'config-volume',
 | 
			
		||||
            configMap: {
 | 
			
		||||
              name: 'shared-config',
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationVolumeConfigsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    // Check main container
 | 
			
		||||
    expect(screen.getByText('main-container')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('/etc/config')).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
    // Check init container
 | 
			
		||||
    expect(screen.getByText('init-container')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('/etc/init-config')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByTestId('asterisk-icon')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('init container')).toBeInTheDocument();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render secret volume configurations correctly based on type', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'test-container',
 | 
			
		||||
            image: 'test-image',
 | 
			
		||||
            volumeMounts: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'secret-volume',
 | 
			
		||||
                mountPath: '/etc/secrets',
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        volumes: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'secret-volume',
 | 
			
		||||
            secret: {
 | 
			
		||||
              secretName: 'test-secret',
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationVolumeConfigsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    expect(screen.getByText('test-container')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('test-secret')).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
    // Should route to secret page because type is 'secret'
 | 
			
		||||
    expect(mockLink).toHaveBeenCalledWith(
 | 
			
		||||
      expect.objectContaining({
 | 
			
		||||
        to: 'kubernetes.secrets.secret',
 | 
			
		||||
        params: {
 | 
			
		||||
          name: 'test-secret',
 | 
			
		||||
          namespace: 'default',
 | 
			
		||||
        },
 | 
			
		||||
        'data-cy': 'secret-link-test-secret',
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render configmap volume configurations correctly based on type', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'test-container',
 | 
			
		||||
            image: 'test-image',
 | 
			
		||||
            volumeMounts: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'config-volume',
 | 
			
		||||
                mountPath: '/etc/config',
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        volumes: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'config-volume',
 | 
			
		||||
            configMap: {
 | 
			
		||||
              name: 'test-configmap',
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationVolumeConfigsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    expect(screen.getByText('test-container')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('test-configmap')).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
    // Should route to configmap page because type is 'configMap'
 | 
			
		||||
    expect(mockLink).toHaveBeenCalledWith(
 | 
			
		||||
      expect.objectContaining({
 | 
			
		||||
        to: 'kubernetes.configmaps.configmap',
 | 
			
		||||
        params: {
 | 
			
		||||
          name: 'test-configmap',
 | 
			
		||||
          namespace: 'default',
 | 
			
		||||
        },
 | 
			
		||||
        'data-cy': 'config-link-test-configmap',
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should handle volumes without items (entire volume mount)', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'test-container',
 | 
			
		||||
            image: 'test-image',
 | 
			
		||||
            volumeMounts: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'config-volume',
 | 
			
		||||
                mountPath: '/etc/config',
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        volumes: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'config-volume',
 | 
			
		||||
            configMap: {
 | 
			
		||||
              name: 'test-configmap',
 | 
			
		||||
              // No items - entire configmap is mounted
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationVolumeConfigsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    expect(screen.getByText('test-container')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('/etc/config')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('-')).toBeInTheDocument(); // No specific key
 | 
			
		||||
    expect(screen.getByText('test-configmap')).toBeInTheDocument();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should handle multiple volumes with different types correctly', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'test-container',
 | 
			
		||||
            image: 'test-image',
 | 
			
		||||
            volumeMounts: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'secret-volume',
 | 
			
		||||
                mountPath: '/etc/secrets',
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                name: 'config-volume',
 | 
			
		||||
                mountPath: '/etc/config',
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        volumes: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'secret-volume',
 | 
			
		||||
            secret: {
 | 
			
		||||
              secretName: 'test-secret',
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'config-volume',
 | 
			
		||||
            configMap: {
 | 
			
		||||
              name: 'test-configmap',
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationVolumeConfigsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    expect(screen.getAllByText('test-container')).toHaveLength(2); // Should appear twice - once for each volume
 | 
			
		||||
    expect(screen.getByText('test-secret')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('test-configmap')).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
    // Should have made two Link calls - one for secret, one for configmap
 | 
			
		||||
    expect(mockLink).toHaveBeenCalledTimes(2);
 | 
			
		||||
 | 
			
		||||
    // Verify secret link
 | 
			
		||||
    expect(mockLink).toHaveBeenCalledWith(
 | 
			
		||||
      expect.objectContaining({
 | 
			
		||||
        to: 'kubernetes.secrets.secret',
 | 
			
		||||
        params: {
 | 
			
		||||
          name: 'test-secret',
 | 
			
		||||
          namespace: 'default',
 | 
			
		||||
        },
 | 
			
		||||
        'data-cy': 'secret-link-test-secret',
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Verify configmap link
 | 
			
		||||
    expect(mockLink).toHaveBeenCalledWith(
 | 
			
		||||
      expect.objectContaining({
 | 
			
		||||
        to: 'kubernetes.configmaps.configmap',
 | 
			
		||||
        params: {
 | 
			
		||||
          name: 'test-configmap',
 | 
			
		||||
          namespace: 'default',
 | 
			
		||||
        },
 | 
			
		||||
        'data-cy': 'config-link-test-configmap',
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should handle containers without volume mounts', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'test-container',
 | 
			
		||||
            image: 'test-image',
 | 
			
		||||
            // No volumeMounts
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        volumes: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'config-volume',
 | 
			
		||||
            configMap: {
 | 
			
		||||
              name: 'test-configmap',
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const { container } = render(
 | 
			
		||||
      <ApplicationVolumeConfigsTable namespace="default" app={app} />
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Should render nothing because there are no matching volume mounts
 | 
			
		||||
    expect(container.firstChild).toBeNull();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should handle Deployment kind applications', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-deployment', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        selector: {
 | 
			
		||||
          matchLabels: {
 | 
			
		||||
            app: 'test-app',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        template: {
 | 
			
		||||
          spec: {
 | 
			
		||||
            containers: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'test-container',
 | 
			
		||||
                image: 'test-image',
 | 
			
		||||
                volumeMounts: [
 | 
			
		||||
                  {
 | 
			
		||||
                    name: 'config-volume',
 | 
			
		||||
                    mountPath: '/etc/config',
 | 
			
		||||
                  },
 | 
			
		||||
                ],
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
            volumes: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'config-volume',
 | 
			
		||||
                configMap: {
 | 
			
		||||
                  name: 'test-configmap',
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Deployment',
 | 
			
		||||
      apiVersion: 'apps/v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationVolumeConfigsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    expect(screen.getByText('test-container')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('/etc/config')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('test-configmap')).toBeInTheDocument();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should handle missing volume config names', () => {
 | 
			
		||||
    const app: Application = {
 | 
			
		||||
      metadata: { name: 'test-pod', namespace: 'default' },
 | 
			
		||||
      spec: {
 | 
			
		||||
        containers: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'test-container',
 | 
			
		||||
            image: 'test-image',
 | 
			
		||||
            volumeMounts: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'config-volume',
 | 
			
		||||
                mountPath: '/etc/config',
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        volumes: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'config-volume',
 | 
			
		||||
            configMap: {
 | 
			
		||||
              // name is undefined
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      kind: 'Pod',
 | 
			
		||||
      apiVersion: 'v1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(<ApplicationVolumeConfigsTable namespace="default" app={app} />);
 | 
			
		||||
 | 
			
		||||
    expect(screen.getByText('test-container')).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText('/etc/config')).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
    // Should show dash for missing volume config name
 | 
			
		||||
    const dashElements = screen.getAllByText('-');
 | 
			
		||||
    expect(dashElements.length).toBeGreaterThan(0);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,15 +1,23 @@
 | 
			
		|||
import { KeyToPath, Pod, Secret } from 'kubernetes-types/core/v1';
 | 
			
		||||
import { KeyToPath, Pod, VolumeMount } from 'kubernetes-types/core/v1';
 | 
			
		||||
import { Asterisk, Plus } from 'lucide-react';
 | 
			
		||||
 | 
			
		||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
 | 
			
		||||
import { useK8sSecrets } from '@/react/kubernetes/configs/queries/useK8sSecrets';
 | 
			
		||||
 | 
			
		||||
import { Icon } from '@@/Icon';
 | 
			
		||||
import { Link } from '@@/Link';
 | 
			
		||||
 | 
			
		||||
import { Application } from '../../types';
 | 
			
		||||
import { applicationIsKind } from '../../utils';
 | 
			
		||||
 | 
			
		||||
type VolumeConfigType = 'configMap' | 'secret';
 | 
			
		||||
 | 
			
		||||
type AppVolumeConfig = {
 | 
			
		||||
  volumeConfigName: string | undefined;
 | 
			
		||||
  containerVolumeMount: VolumeMount | undefined;
 | 
			
		||||
  containerName: string;
 | 
			
		||||
  isInitContainer: boolean;
 | 
			
		||||
  item: KeyToPath;
 | 
			
		||||
  type: VolumeConfigType;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
  namespace: string;
 | 
			
		||||
  app?: Application;
 | 
			
		||||
| 
						 | 
				
			
			@ -18,8 +26,6 @@ type Props = {
 | 
			
		|||
export function ApplicationVolumeConfigsTable({ namespace, app }: Props) {
 | 
			
		||||
  const containerVolumeConfigs = getApplicationVolumeConfigs(app);
 | 
			
		||||
 | 
			
		||||
  const { data: secrets } = useK8sSecrets(useEnvironmentId(), namespace);
 | 
			
		||||
 | 
			
		||||
  if (containerVolumeConfigs.length === 0) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -41,6 +47,7 @@ export function ApplicationVolumeConfigsTable({ namespace, app }: Props) {
 | 
			
		|||
              containerName,
 | 
			
		||||
              item,
 | 
			
		||||
              volumeConfigName,
 | 
			
		||||
              type,
 | 
			
		||||
            },
 | 
			
		||||
            index
 | 
			
		||||
          ) => (
 | 
			
		||||
| 
						 | 
				
			
			@ -76,7 +83,7 @@ export function ApplicationVolumeConfigsTable({ namespace, app }: Props) {
 | 
			
		|||
                {!item.key && '-'}
 | 
			
		||||
              </td>
 | 
			
		||||
              <td>
 | 
			
		||||
                {isVolumeConfigNameFromSecret(secrets, volumeConfigName) ? (
 | 
			
		||||
                {type === 'secret' ? (
 | 
			
		||||
                  <Link
 | 
			
		||||
                    className="flex items-center"
 | 
			
		||||
                    to="kubernetes.secrets.secret"
 | 
			
		||||
| 
						 | 
				
			
			@ -107,15 +114,8 @@ export function ApplicationVolumeConfigsTable({ namespace, app }: Props) {
 | 
			
		|||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isVolumeConfigNameFromSecret(
 | 
			
		||||
  secrets?: Secret[],
 | 
			
		||||
  volumeConfigName?: string
 | 
			
		||||
) {
 | 
			
		||||
  return secrets?.some((secret) => secret.metadata?.name === volumeConfigName);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getApplicationVolumeConfigs returns a list of volume configs / secrets for each container and each item within the matching volume
 | 
			
		||||
function getApplicationVolumeConfigs(app?: Application) {
 | 
			
		||||
function getApplicationVolumeConfigs(app?: Application): AppVolumeConfig[] {
 | 
			
		||||
  if (!app) {
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -142,6 +142,10 @@ function getApplicationVolumeConfigs(app?: Application) {
 | 
			
		|||
        const containerVolumeMount = container.volumeMounts?.find(
 | 
			
		||||
          (volumeMount) => volumeMount.name === volume.name
 | 
			
		||||
        );
 | 
			
		||||
        const type: VolumeConfigType = volume.configMap
 | 
			
		||||
          ? 'configMap'
 | 
			
		||||
          : 'secret';
 | 
			
		||||
 | 
			
		||||
        if (volConfigMapItems.length === 0) {
 | 
			
		||||
          return [
 | 
			
		||||
            {
 | 
			
		||||
| 
						 | 
				
			
			@ -150,6 +154,7 @@ function getApplicationVolumeConfigs(app?: Application) {
 | 
			
		|||
              containerName: container.name,
 | 
			
		||||
              isInitContainer: appInitContainers.includes(container),
 | 
			
		||||
              item: {} as KeyToPath,
 | 
			
		||||
              type,
 | 
			
		||||
            },
 | 
			
		||||
          ];
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -160,6 +165,7 @@ function getApplicationVolumeConfigs(app?: Application) {
 | 
			
		|||
          containerName: container.name,
 | 
			
		||||
          isInitContainer: appInitContainers.includes(container),
 | 
			
		||||
          item,
 | 
			
		||||
          type,
 | 
			
		||||
        }));
 | 
			
		||||
      })
 | 
			
		||||
      // only return the app volumes where the container volumeMounts include the volume name (from map step above)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue