Skip to content

Commit

Permalink
feat(frontend): Add "Test Credentials" button for profiles
Browse files Browse the repository at this point in the history
This commit introduces unit tests to verify the validity of profile
credentials. It also incorporates a "Test Credentials" button on the
profile data table that enables users to verify their credentials.
The feature employs the `validate_credentials` feature of the
backend, which ensures the accuracy of the user's profile details.'

Refs: #14
  • Loading branch information
maikbasel committed Mar 7, 2024
1 parent 8ef1ea7 commit 9b9104c
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 41 deletions.
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"lint:fix": "next lint --fix",
"lint:report": "next lint --output-file eslint_report.json --format json",
"tauri": "tauri",
"tauri:dev": "npm run tauri dev"
"tauri:dev": "cross-env LOCALSTACK_ENDPOINT=http://localhost:4566 npm run tauri dev"
},
"dependencies": {
"@radix-ui/react-accordion": "^1.1.2",
Expand All @@ -46,6 +46,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"cross-env": "^7.0.3",
"eslint": "8.47.0",
"eslint-config-next": "13.4.19",
"lucide-react": "^0.303.0",
Expand Down
65 changes: 52 additions & 13 deletions src/sections/profiles/components/profile-data-table.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import { ProfileSet } from '@/modules/profiles/domain';
import { ProfileDataTable } from '@/sections/profiles/components/profile-data-table';
import userEvent from '@testing-library/user-event';
import { SWRConfig } from 'swr';
import { mockIPC } from '@tauri-apps/api/mocks';

describe('<ProfileDataTable />', () => {
const profileSet: ProfileSet = {
Expand All @@ -21,19 +23,36 @@ describe('<ProfileDataTable />', () => {
errors: {},
};

it('should render profile data table with profile set data', () => {
render(<ProfileDataTable data={profileSet} />);
beforeEach(() => {
mockIPC((cmd) => {
if (cmd === 'validate_credentials') {
return false;
}
});
});

it('should render profile data table with profile set data', async () => {
render(
<SWRConfig value={{ provider: () => new Map() }}>
<ProfileDataTable data={profileSet} />
</SWRConfig>
);
const row = screen.getByText(/prof1/i).closest('tr');

expect(row).toHaveTextContent('key1');
expect(row).toHaveTextContent('secret1');
expect(row).toHaveTextContent('eu-west-1');
expect(row).toHaveTextContent('json');
await waitFor(() => {
expect(row).toHaveTextContent('key1');
expect(row).toHaveTextContent('secret1');
expect(row).toHaveTextContent('eu-west-1');
expect(row).toHaveTextContent('json');
});
});

it('should select all rows when the "Select All" checkbox is clicked', async () => {
render(<ProfileDataTable data={profileSet} />);
render(
<SWRConfig value={{ provider: () => new Map() }}>
<ProfileDataTable data={profileSet} />
</SWRConfig>
);

await userEvent.click(screen.getByRole('checkbox', { name: 'Select all' }));

Expand All @@ -43,7 +62,11 @@ describe('<ProfileDataTable />', () => {
});

it('given "Select All" checkbox is already checked then should unselect all rows when the "Select All" checkbox is clicked again', async () => {
render(<ProfileDataTable data={profileSet} />);
render(
<SWRConfig value={{ provider: () => new Map() }}>
<ProfileDataTable data={profileSet} />
</SWRConfig>
);

await userEvent.click(screen.getByRole('checkbox', { name: 'Select all' }));

Expand All @@ -55,7 +78,11 @@ describe('<ProfileDataTable />', () => {
});

it('should select row 1 when the "Select row 1" checkbox is clicked', async () => {
render(<ProfileDataTable data={profileSet} />);
render(
<SWRConfig value={{ provider: () => new Map() }}>
<ProfileDataTable data={profileSet} />
</SWRConfig>
);
const row1Checkbox = screen.getByRole('checkbox', { name: 'Select row 1' });

await userEvent.click(row1Checkbox);
Expand All @@ -64,7 +91,11 @@ describe('<ProfileDataTable />', () => {
});

it('given row 1 is checkbox is already selected then should unselect row 1 when the "Select row 1" checkbox is clicked again', async () => {
render(<ProfileDataTable data={profileSet} />);
render(
<SWRConfig value={{ provider: () => new Map() }}>
<ProfileDataTable data={profileSet} />
</SWRConfig>
);
const row1Checkbox = screen.getByRole('checkbox', { name: 'Select row 1' });

await userEvent.click(row1Checkbox);
Expand Down Expand Up @@ -100,7 +131,11 @@ describe('<ProfileDataTable />', () => {
},
errors: {},
};
render(<ProfileDataTable data={input} />);
render(
<SWRConfig value={{ provider: () => new Map() }}>
<ProfileDataTable data={input} />
</SWRConfig>
);

expect(screen.getAllByRole('row').length).toBe(3);

Expand Down Expand Up @@ -139,7 +174,11 @@ describe('<ProfileDataTable />', () => {
},
errors: {},
};
render(<ProfileDataTable data={input} />);
render(
<SWRConfig value={{ provider: () => new Map() }}>
<ProfileDataTable data={input} />
</SWRConfig>
);

expect(screen.getAllByRole('row').length).toBe(3);

Expand Down
31 changes: 4 additions & 27 deletions src/sections/profiles/components/profile-data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,14 @@ import React from 'react';
import { ColumnDef } from '@tanstack/table-core';
import { ProfileSet } from '@/modules/profiles/domain';
import { DataTable } from '@/components/ui/data-table';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Button } from '@/components/ui/button';
import { FileType, Globe2Icon, LucideIcon, MoreHorizontal } from 'lucide-react';
import { FileType, Globe2Icon, LucideIcon } from 'lucide-react';
import { DataTableColumnHeader } from '@/components/ui/data-table-column-header';
import { Checkbox } from '@/components/ui/checkbox';
import {
FilterableColumn,
SearchInputFilter,
} from '@/components/ui/data-table-toolbar';
import TestCredentialsButton from '@/sections/profiles/components/test-credentials-button';

export type Profile = {
name: string;
Expand Down Expand Up @@ -91,26 +83,11 @@ const profileColumns: ColumnDef<Profile>[] = [
},
},
{
id: 'actions',
id: 'test',
cell: ({ row }) => {
const profile = row.original;

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='ghost' className='h-8 w-8 p-0'>
<span className='sr-only'>Open menu</span>
<MoreHorizontal className='h-4 w-4' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Update {profile.name} profile</DropdownMenuItem>
<DropdownMenuItem>Delete {profile.name} profile</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
return <TestCredentialsButton profile={profile.name} />;
},
},
];
Expand Down
50 changes: 50 additions & 0 deletions src/sections/profiles/components/test-credentials-button.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import TestCredentialsButton from './test-credentials-button';
import { clearMocks, mockIPC } from '@tauri-apps/api/mocks';
import { SWRConfig } from 'swr';
import userEvent from '@testing-library/user-event';

describe('<TestCredentialsButton />', () => {
afterEach(() => {
clearMocks();
});

it('should render shield alert icon when profile credentials are invalid', async () => {
mockIPC((cmd) => {
if (cmd === 'validate_credentials') {
return false;
}
});
render(
<SWRConfig value={{ provider: () => new Map() }}>
<TestCredentialsButton profile='prof1' />
</SWRConfig>
);

const button = screen.getByRole('button');
await userEvent.click(button);

const shieldSvg = button.querySelector('svg');
expect(shieldSvg).toHaveClass('lucide-shield-alert');
});

it('should render shield check icon when profile credentials are valid', async () => {
mockIPC((cmd) => {
if (cmd === 'validate_credentials') {
return true;
}
});
render(
<SWRConfig value={{ provider: () => new Map() }}>
<TestCredentialsButton profile='prof1' />
</SWRConfig>
);

const button = screen.getByRole('button');
await userEvent.click(button);

const shieldSvg = button.querySelector('svg');
expect(shieldSvg).toHaveClass('lucide-shield-check');
});
});
61 changes: 61 additions & 0 deletions src/sections/profiles/components/test-credentials-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use client';

import { Shield, ShieldAlert, ShieldCheck } from 'lucide-react';
import { Button } from '@/components/ui/button';
import React, { useEffect, useState } from 'react';
import { invoke } from '@tauri-apps/api/tauri';

type TestCredentialsButtonProps = {
profile: string;
};

export default function TestCredentialsButton({
profile,
}: Readonly<TestCredentialsButtonProps>) {
const [validated, setValidated] = useState<boolean>(false);
const [busy, setBusy] = useState<boolean>(false);
const [valid, setValid] = useState<boolean>(false);

useEffect(() => {
setValidated(true);
setBusy(true);

invoke<boolean>('validate_credentials', {
profileName: profile,
}).then((value) => {
setValid(value);
});

setBusy(false);
}, []);

Check warning on line 30 in src/sections/profiles/components/test-credentials-button.tsx

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

src/sections/profiles/components/test-credentials-button.tsx#L30

[react-hooks/exhaustive-deps] React Hook useEffect has a missing dependency: 'profile'. Either include it or remove the dependency array.

return (
<Button
variant='outline'
size='icon'
disabled={busy}
onClick={() => {
setValidated(true);
setBusy(true);
setTimeout(async () => {
invoke<boolean>('validate_credentials', {
profileName: profile,
}).then((value) => {
setValid(value);
});
setBusy(false);
}, 1000);
}}
>
{validated ? (
valid ? (
<ShieldCheck className={`h-4 w-4 ${busy ? 'animate-pulse' : ''}`} />
) : (
<ShieldAlert className={`h-4 w-4 ${busy ? 'animate-pulse' : ''}`} />
)
) : (
<Shield className='h-4 w-4' />
)}
</Button>
);
}

0 comments on commit 9b9104c

Please sign in to comment.