Skip to content

Commit acc7037

Browse files
nullvariantclaude
andauthored
feat(identity): add deleteIdentityFromConfig function (#238)
Add core function to delete an identity from VS Code configuration. Features: - ID format validation using isValidIdentityId() - Error handling for invalid ID format and non-existent identity - Security logging via securityLogger.logConfigChange() - Uses VS Code Configuration API (no direct file operations) Tests added (6 test cases): - Delete existing identity successfully - Preserve other identities after deletion - Error for non-existent identity ID - Error for empty string ID - Error for invalid ID format (spaces, special chars) - Error when deleting from empty identities array 🖥️ IDE: [Cursor](https://cursor.sh) 🔌 Extension: [Claude Code](https://claude.ai/download) Model-Raw: claude-opus-4-5-20251101 Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0b99e88 commit acc7037

2 files changed

Lines changed: 192 additions & 1 deletion

File tree

extensions/git-id-switcher/src/identity/identity.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
import * as vscode from 'vscode';
99
import { validateIdentitySchema } from './configSchema';
1010
import { securityLogger } from '../security/securityLogger';
11-
import { MAX_IDENTITIES } from '../core/constants';
11+
import { MAX_IDENTITIES, MAX_ID_LENGTH } from '../core/constants';
12+
import { isValidIdentityId } from '../validators/common';
1213

1314
/**
1415
* Session-level flag to prevent multiple validation error notifications.
@@ -253,3 +254,40 @@ export function formatGitAuthor(identity: Identity): string {
253254
}
254255
return `${identity.name} <${identity.email}>`;
255256
}
257+
258+
/**
259+
* Delete an identity from VS Code configuration.
260+
*
261+
* @param id - The identity ID to delete (must pass isValidIdentityId validation)
262+
* @returns Promise that resolves when deletion is complete
263+
* @throws Error if identity ID format is invalid
264+
* @throws Error if identity with given ID is not found
265+
* @throws Error if configuration update fails
266+
*
267+
* @example
268+
* await deleteIdentityFromConfig('work-github');
269+
*/
270+
export async function deleteIdentityFromConfig(id: string): Promise<void> {
271+
// Validate ID format
272+
if (!isValidIdentityId(id, MAX_ID_LENGTH)) {
273+
throw new Error(`Invalid identity ID format: ${id}`);
274+
}
275+
276+
const config = vscode.workspace.getConfiguration('gitIdSwitcher');
277+
const identities = config.get<Identity[]>('identities', []);
278+
279+
// Find the identity to delete
280+
const index = identities.findIndex(i => i.id === id);
281+
if (index === -1) {
282+
throw new Error(`Identity not found: ${id}`);
283+
}
284+
285+
// Remove the identity from array
286+
const updatedIdentities = identities.filter(i => i.id !== id);
287+
288+
// Update configuration
289+
await config.update('identities', updatedIdentities, vscode.ConfigurationTarget.Global);
290+
291+
// Log security event
292+
securityLogger.logConfigChange('identities');
293+
}

extensions/git-id-switcher/src/test/e2e/identity.test.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import * as assert from 'node:assert';
2323
import * as vscode from 'vscode';
24+
import { deleteIdentityFromConfig } from '../../identity/identity';
2425

2526
const EXTENSION_ID = 'nullvariant.git-id-switcher';
2627
const CONFIG_SECTION = 'gitIdSwitcher';
@@ -706,4 +707,156 @@ describe('Identity E2E Test Suite', function () {
706707
assert.strictEqual(inspection?.key, 'gitIdSwitcher.identities', 'Key should be fully qualified');
707708
});
708709
});
710+
711+
describe('deleteIdentityFromConfig', () => {
712+
it('should delete an existing identity successfully', async () => {
713+
const config = vscode.workspace.getConfiguration(CONFIG_SECTION);
714+
const currentIdentities = config.get<TestIdentity[]>('identities', []);
715+
716+
// Add a test identity to delete
717+
const testIdentity: TestIdentity = {
718+
id: 'test-delete-identity',
719+
name: 'Delete Test User',
720+
email: 'delete-test@example.com',
721+
};
722+
const identitiesWithTest = [...currentIdentities, testIdentity];
723+
await config.update('identities', identitiesWithTest, vscode.ConfigurationTarget.Global);
724+
725+
// Verify test identity was added
726+
const configAfterAdd = vscode.workspace.getConfiguration(CONFIG_SECTION);
727+
const afterAdd = configAfterAdd.get<TestIdentity[]>('identities', []);
728+
assert.ok(
729+
afterAdd.some(i => i.id === 'test-delete-identity'),
730+
'Test identity should be added before deletion'
731+
);
732+
733+
// Delete the test identity
734+
await deleteIdentityFromConfig('test-delete-identity');
735+
736+
// Verify identity was deleted
737+
const freshConfig = vscode.workspace.getConfiguration(CONFIG_SECTION);
738+
const afterDelete = freshConfig.get<TestIdentity[]>('identities', []);
739+
assert.ok(
740+
!afterDelete.some(i => i.id === 'test-delete-identity'),
741+
'Test identity should be deleted'
742+
);
743+
744+
// Restore original state
745+
await config.update('identities', currentIdentities, vscode.ConfigurationTarget.Global);
746+
});
747+
748+
it('should preserve other identities after deletion', async () => {
749+
const config = vscode.workspace.getConfiguration(CONFIG_SECTION);
750+
const currentIdentities = config.get<TestIdentity[]>('identities', []);
751+
752+
// Add multiple test identities
753+
const testIdentities: TestIdentity[] = [
754+
{ id: 'test-keep-1', name: 'Keep User 1', email: 'keep1@example.com' },
755+
{ id: 'test-delete-me', name: 'Delete Me', email: 'delete@example.com' },
756+
{ id: 'test-keep-2', name: 'Keep User 2', email: 'keep2@example.com' },
757+
];
758+
await config.update('identities', testIdentities, vscode.ConfigurationTarget.Global);
759+
760+
// Delete the middle identity
761+
await deleteIdentityFromConfig('test-delete-me');
762+
763+
// Verify correct identities remain
764+
const freshConfig = vscode.workspace.getConfiguration(CONFIG_SECTION);
765+
const afterDelete = freshConfig.get<TestIdentity[]>('identities', []);
766+
assert.strictEqual(afterDelete.length, 2, 'Should have 2 identities remaining');
767+
assert.ok(afterDelete.some(i => i.id === 'test-keep-1'), 'First identity should remain');
768+
assert.ok(afterDelete.some(i => i.id === 'test-keep-2'), 'Second identity should remain');
769+
assert.ok(!afterDelete.some(i => i.id === 'test-delete-me'), 'Deleted identity should not exist');
770+
771+
// Restore original state
772+
await config.update('identities', currentIdentities, vscode.ConfigurationTarget.Global);
773+
});
774+
775+
it('should throw error for non-existent identity ID', async () => {
776+
const config = vscode.workspace.getConfiguration(CONFIG_SECTION);
777+
const currentIdentities = config.get<TestIdentity[]>('identities', []);
778+
779+
// Set up a known state
780+
const testIdentity: TestIdentity = {
781+
id: 'existing-identity',
782+
name: 'Existing User',
783+
email: 'existing@example.com',
784+
};
785+
await config.update('identities', [testIdentity], vscode.ConfigurationTarget.Global);
786+
787+
// Try to delete non-existent identity
788+
try {
789+
await deleteIdentityFromConfig('non-existent-id');
790+
assert.fail('Should have thrown an error');
791+
} catch (error) {
792+
assert.ok(error instanceof Error, 'Should throw an Error');
793+
assert.ok(
794+
error.message.includes('not found'),
795+
`Error message should indicate identity not found, got: ${error.message}`
796+
);
797+
}
798+
799+
// Restore original state
800+
await config.update('identities', currentIdentities, vscode.ConfigurationTarget.Global);
801+
});
802+
803+
it('should throw error for empty string ID', async () => {
804+
try {
805+
await deleteIdentityFromConfig('');
806+
assert.fail('Should have thrown an error');
807+
} catch (error) {
808+
assert.ok(error instanceof Error, 'Should throw an Error');
809+
assert.ok(
810+
error.message.includes('Invalid'),
811+
`Error message should indicate invalid ID, got: ${error.message}`
812+
);
813+
}
814+
});
815+
816+
it('should throw error for invalid ID format', async () => {
817+
// Test with characters that are not allowed in identity IDs
818+
const invalidIds = [
819+
'id with spaces',
820+
'id@with@symbols',
821+
'id/with/slashes',
822+
'id$with$dollar',
823+
];
824+
825+
for (const invalidId of invalidIds) {
826+
try {
827+
await deleteIdentityFromConfig(invalidId);
828+
assert.fail(`Should have thrown an error for invalid ID: ${invalidId}`);
829+
} catch (error) {
830+
assert.ok(error instanceof Error, 'Should throw an Error');
831+
assert.ok(
832+
error.message.includes('Invalid'),
833+
`Error message should indicate invalid ID format for "${invalidId}", got: ${error.message}`
834+
);
835+
}
836+
}
837+
});
838+
839+
it('should throw error when deleting from empty identities array', async () => {
840+
const config = vscode.workspace.getConfiguration(CONFIG_SECTION);
841+
const currentIdentities = config.get<TestIdentity[]>('identities', []);
842+
843+
// Set empty identities array
844+
await config.update('identities', [], vscode.ConfigurationTarget.Global);
845+
846+
// Try to delete from empty array
847+
try {
848+
await deleteIdentityFromConfig('any-valid-id');
849+
assert.fail('Should have thrown an error');
850+
} catch (error) {
851+
assert.ok(error instanceof Error, 'Should throw an Error');
852+
assert.ok(
853+
error.message.includes('not found'),
854+
`Error message should indicate identity not found, got: ${error.message}`
855+
);
856+
}
857+
858+
// Restore original state
859+
await config.update('identities', currentIdentities, vscode.ConfigurationTarget.Global);
860+
});
861+
});
709862
});

0 commit comments

Comments
 (0)