mirror of
https://github.com/selfxyz/self.git
synced 2026-01-10 15:18:18 -05:00
Show badge for inactive documents
This commit is contained in:
@@ -4,19 +4,23 @@
|
||||
|
||||
import type { FC } from 'react';
|
||||
import React from 'react';
|
||||
import { Dimensions } from 'react-native';
|
||||
import { Alert, Dimensions, Pressable } from 'react-native';
|
||||
import { Separator, Text, XStack, YStack } from 'tamagui';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import type { AadhaarData } from '@selfxyz/common';
|
||||
import type { PassportData } from '@selfxyz/common/types/passport';
|
||||
import { isAadhaarDocument, isMRZDocument } from '@selfxyz/common/utils/types';
|
||||
import { WarningTriangleIcon } from '@selfxyz/euclid/dist/components/icons/WarningTriangleIcon';
|
||||
import {
|
||||
black,
|
||||
red600,
|
||||
slate100,
|
||||
slate300,
|
||||
slate400,
|
||||
slate500,
|
||||
white,
|
||||
yellow500,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot, plexMono } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
import AadhaarIcon from '@selfxyz/mobile-sdk-alpha/svgs/icons/aadhaar.svg';
|
||||
@@ -41,6 +45,7 @@ interface IdCardLayoutAttributes {
|
||||
idDocument: PassportData | AadhaarData | null;
|
||||
selected: boolean;
|
||||
hidden: boolean;
|
||||
isInactive: boolean;
|
||||
}
|
||||
|
||||
// This layout should be fully adaptative. I should perfectly fit in any screen size.
|
||||
@@ -53,7 +58,10 @@ const IdCardLayout: FC<IdCardLayoutAttributes> = ({
|
||||
idDocument,
|
||||
selected,
|
||||
hidden,
|
||||
isInactive,
|
||||
}) => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
// Early return if document is null
|
||||
if (!idDocument) {
|
||||
return null;
|
||||
@@ -96,6 +104,54 @@ const IdCardLayout: FC<IdCardLayoutAttributes> = ({
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
{isInactive && (
|
||||
<Pressable
|
||||
style={{ width: '100%', marginBottom: 16 }}
|
||||
onPress={() => {
|
||||
switch (idDocument.documentCategory) {
|
||||
case 'passport':
|
||||
case 'id_card':
|
||||
navigation.navigate('DocumentOnboarding');
|
||||
break;
|
||||
case 'aadhaar':
|
||||
navigation.navigate('AadhaarUpload', { countryCode: 'IND' });
|
||||
break;
|
||||
default:
|
||||
navigation.navigate('CountryPicker');
|
||||
break;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<XStack
|
||||
backgroundColor={red600}
|
||||
borderRadius={8}
|
||||
padding={16}
|
||||
gap={16}
|
||||
>
|
||||
<YStack padding={8} backgroundColor={white} borderRadius={8}>
|
||||
<WarningTriangleIcon color={yellow500} />
|
||||
</YStack>
|
||||
<YStack gap={4}>
|
||||
<Text
|
||||
color={white}
|
||||
fontFamily={dinot}
|
||||
fontSize={16}
|
||||
fontWeight="500"
|
||||
>
|
||||
Your document is inactive
|
||||
</Text>
|
||||
<Text
|
||||
color={white}
|
||||
fontFamily={dinot}
|
||||
fontSize={14}
|
||||
fontWeight="400"
|
||||
>
|
||||
Tap here to recover your ID
|
||||
</Text>
|
||||
</YStack>
|
||||
</XStack>
|
||||
</Pressable>
|
||||
)}
|
||||
<YStack
|
||||
width={cardWidth}
|
||||
height={cardHeight}
|
||||
@@ -177,6 +233,29 @@ const IdCardLayout: FC<IdCardLayoutAttributes> = ({
|
||||
</Text>
|
||||
</YStack>
|
||||
)}
|
||||
|
||||
{isInactive && (
|
||||
<YStack
|
||||
marginTop={padding / 4}
|
||||
marginLeft={padding / 2}
|
||||
borderWidth={1}
|
||||
borderColor={red600}
|
||||
backgroundColor={red600}
|
||||
borderRadius={100}
|
||||
paddingHorizontal={padding / 2}
|
||||
alignSelf="flex-start"
|
||||
paddingVertical={padding / 8}
|
||||
>
|
||||
<Text
|
||||
fontSize={fontSize.xsmall}
|
||||
color={white}
|
||||
fontFamily={dinot}
|
||||
letterSpacing={fontSize.xsmall * 0.15}
|
||||
>
|
||||
INACTIVE
|
||||
</Text>
|
||||
</YStack>
|
||||
)}
|
||||
</XStack>
|
||||
</XStack>
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ import type { RootStackParamList } from '@/navigation';
|
||||
import { usePassport } from '@/providers/passportDataProvider';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
import useUserStore from '@/stores/userStore';
|
||||
import { isDocumentInactive } from '@/utils/documents';
|
||||
|
||||
const HomeScreen: React.FC = () => {
|
||||
const selfClient = useSelfClient();
|
||||
@@ -69,6 +70,9 @@ const HomeScreen: React.FC = () => {
|
||||
>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const hasIncrementedOnFocus = useRef(false);
|
||||
const [isSelectedDocumentInactive, setIsSelectedDocumentInactive] = useState<
|
||||
boolean | null
|
||||
>(null);
|
||||
|
||||
const { amount: selfPoints } = usePoints();
|
||||
|
||||
@@ -108,12 +112,32 @@ const HomeScreen: React.FC = () => {
|
||||
|
||||
const loadDocuments = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const catalog = await loadDocumentCatalog();
|
||||
const docs = await getAllDocuments();
|
||||
|
||||
setDocumentCatalog(catalog);
|
||||
setAllDocuments(docs);
|
||||
|
||||
if (catalog.selectedDocumentId) {
|
||||
const documentData = docs[catalog.selectedDocumentId];
|
||||
|
||||
if (documentData) {
|
||||
try {
|
||||
setIsSelectedDocumentInactive(
|
||||
await isDocumentInactive(
|
||||
selfClient,
|
||||
documentData.data,
|
||||
documentData.metadata,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
// we don't want to block the home screen from loading
|
||||
console.warn('Failed to check if document is inactive:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load documents:', error);
|
||||
}
|
||||
@@ -254,6 +278,8 @@ const HomeScreen: React.FC = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
// const isInactive = await isDocumentInactive(selfClient, documentData.data, secret);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
key={metadata.id}
|
||||
@@ -261,6 +287,7 @@ const HomeScreen: React.FC = () => {
|
||||
>
|
||||
<IdCardLayout
|
||||
idDocument={documentData.data}
|
||||
isInactive={isSelected && isSelectedDocumentInactive === true}
|
||||
selected={isSelected}
|
||||
hidden={true}
|
||||
/>
|
||||
|
||||
109
app/src/utils/documents.ts
Normal file
109
app/src/utils/documents.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type {
|
||||
DocumentCategory,
|
||||
Environment,
|
||||
IDDocument,
|
||||
} from '@selfxyz/common';
|
||||
import { isUserRegisteredWithAlternativeCSCA } from '@selfxyz/common/utils/passports/validate';
|
||||
import type { DocumentMetadata, SelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { getCommitmentTree } from '@selfxyz/mobile-sdk-alpha/stores';
|
||||
|
||||
import { getAlternativeCSCA } from '@/proving/validateDocument';
|
||||
|
||||
const REGISTERED_THRESHOLD_MINUTES = 5;
|
||||
|
||||
// Map to keep track of which commitment trees have already been fetched so
|
||||
// we don't fetch them multiple times
|
||||
const ALREADY_FETCHED_COMMITMENT_TREES = new Map<DocumentCategory, boolean>();
|
||||
|
||||
/**
|
||||
* Resets the commitment tree cache. Only for testing purposes.
|
||||
* @internal
|
||||
*/
|
||||
export function _resetCommitmentTreeCache(): void {
|
||||
ALREADY_FETCHED_COMMITMENT_TREES.clear();
|
||||
}
|
||||
|
||||
const fetchRequiredCommitmentTree = async (
|
||||
selfClient: SelfClient,
|
||||
document: IDDocument,
|
||||
environment: Environment,
|
||||
) => {
|
||||
if (
|
||||
ALREADY_FETCHED_COMMITMENT_TREES.get(document.documentCategory) === true
|
||||
) {
|
||||
console.log(`${document.documentCategory} commitment tree already fetched`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.documentCategory === 'aadhaar') {
|
||||
await selfClient.getProtocolState().aadhaar.fetch_all(environment);
|
||||
} else {
|
||||
await selfClient
|
||||
.getProtocolState()
|
||||
[
|
||||
document.documentCategory
|
||||
].fetch_all(environment, document.dsc_parsed?.authorityKeyIdentifier || '');
|
||||
}
|
||||
|
||||
ALREADY_FETCHED_COMMITMENT_TREES.set(document.documentCategory, true);
|
||||
};
|
||||
|
||||
export async function isDocumentInactive(
|
||||
selfClient: SelfClient,
|
||||
document: IDDocument,
|
||||
metadata: DocumentMetadata,
|
||||
): Promise<boolean> {
|
||||
console.log(
|
||||
`Checking if ${document.documentCategory} document is inactive...`,
|
||||
);
|
||||
const secret = await selfClient.getPrivateKey();
|
||||
|
||||
if (metadata.registeredAt) {
|
||||
const registeredAt = new Date(metadata.registeredAt);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - registeredAt.getTime();
|
||||
const diffMinutes = diffMs / (1000 * 60);
|
||||
|
||||
if (diffMinutes < REGISTERED_THRESHOLD_MINUTES) {
|
||||
console.log('Document has been registered in the last 5 minutes');
|
||||
// We consider the document active if it was registered in the last 5 minutes
|
||||
// before checking the commitment tree
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const environment = metadata.mock ? 'stg' : 'prod';
|
||||
|
||||
await fetchRequiredCommitmentTree(selfClient, document, environment);
|
||||
|
||||
// if secret is not available, the document is considered inactive
|
||||
if (!secret) {
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Checking if document is registered in the ${document.documentCategory} commitment tree...`,
|
||||
);
|
||||
|
||||
const { isRegistered } = await isUserRegisteredWithAlternativeCSCA(
|
||||
document,
|
||||
secret,
|
||||
{
|
||||
getCommitmentTree: docCategory =>
|
||||
getCommitmentTree(selfClient, docCategory),
|
||||
getAltCSCA: docCategory =>
|
||||
getAlternativeCSCA(selfClient.useProtocolStore, docCategory),
|
||||
},
|
||||
);
|
||||
|
||||
console.log('Document is registered:', isRegistered);
|
||||
|
||||
// TODO: add expiration check
|
||||
|
||||
return !isRegistered;
|
||||
}
|
||||
427
app/tests/src/utils/documents.test.ts
Normal file
427
app/tests/src/utils/documents.test.ts
Normal file
@@ -0,0 +1,427 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type { IDDocument } from '@selfxyz/common';
|
||||
import type { DocumentMetadata, SelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
|
||||
// Import module under test after mocks are set up
|
||||
import {
|
||||
_resetCommitmentTreeCache,
|
||||
isDocumentInactive,
|
||||
} from '@/utils/documents';
|
||||
|
||||
// Mock dependencies before importing the module under test
|
||||
const mockGetAlternativeCSCA = jest.fn();
|
||||
jest.mock('@/proving/validateDocument', () => ({
|
||||
getAlternativeCSCA: jest.fn((...args: unknown[]) =>
|
||||
mockGetAlternativeCSCA(...args),
|
||||
),
|
||||
}));
|
||||
|
||||
const mockIsUserRegisteredWithAlternativeCSCA = jest.fn();
|
||||
jest.mock('@selfxyz/common/utils/passports/validate', () => ({
|
||||
isUserRegisteredWithAlternativeCSCA: jest.fn((...args: unknown[]) =>
|
||||
mockIsUserRegisteredWithAlternativeCSCA(...args),
|
||||
),
|
||||
}));
|
||||
|
||||
const mockGetCommitmentTree = jest.fn();
|
||||
jest.mock('@selfxyz/mobile-sdk-alpha/stores', () => ({
|
||||
getCommitmentTree: jest.fn((...args: unknown[]) =>
|
||||
mockGetCommitmentTree(...args),
|
||||
),
|
||||
}));
|
||||
|
||||
describe('isDocumentInactive', () => {
|
||||
// Mock implementations
|
||||
const mockFetchAllPassport = jest.fn();
|
||||
const mockFetchAllIdCard = jest.fn();
|
||||
const mockFetchAllAadhaar = jest.fn();
|
||||
const mockGetPrivateKey = jest.fn();
|
||||
|
||||
const createMockSelfClient = (): SelfClient =>
|
||||
({
|
||||
getPrivateKey: mockGetPrivateKey,
|
||||
getProtocolState: jest.fn(() => ({
|
||||
passport: { fetch_all: mockFetchAllPassport },
|
||||
id_card: { fetch_all: mockFetchAllIdCard },
|
||||
aadhaar: { fetch_all: mockFetchAllAadhaar },
|
||||
})),
|
||||
useProtocolStore: {},
|
||||
}) as unknown as SelfClient;
|
||||
|
||||
const createMockDocument = (
|
||||
category: 'passport' | 'id_card' | 'aadhaar',
|
||||
authorityKeyIdentifier?: string,
|
||||
): IDDocument =>
|
||||
({
|
||||
documentCategory: category,
|
||||
dsc_parsed: authorityKeyIdentifier
|
||||
? { authorityKeyIdentifier }
|
||||
: undefined,
|
||||
}) as unknown as IDDocument;
|
||||
|
||||
const createMockMetadata = (
|
||||
overrides: Partial<DocumentMetadata> = {},
|
||||
): DocumentMetadata =>
|
||||
({
|
||||
id: 'test-doc-id',
|
||||
documentType: 'passport',
|
||||
documentCategory: 'passport',
|
||||
mock: false,
|
||||
isRegistered: true,
|
||||
registeredAt: undefined,
|
||||
...overrides,
|
||||
}) as DocumentMetadata;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Reset the commitment tree cache to ensure test isolation
|
||||
_resetCommitmentTreeCache();
|
||||
// Reset all mock implementations to default successful state
|
||||
mockGetPrivateKey.mockResolvedValue('test-secret');
|
||||
mockFetchAllPassport.mockResolvedValue(undefined);
|
||||
mockFetchAllIdCard.mockResolvedValue(undefined);
|
||||
mockFetchAllAadhaar.mockResolvedValue(undefined);
|
||||
mockIsUserRegisteredWithAlternativeCSCA.mockResolvedValue({
|
||||
isRegistered: true,
|
||||
});
|
||||
mockGetAlternativeCSCA.mockReturnValue(null);
|
||||
mockGetCommitmentTree.mockReturnValue('mock-commitment-tree');
|
||||
});
|
||||
|
||||
describe('recently registered documents', () => {
|
||||
it('returns false when document was registered within last 5 minutes', async () => {
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('passport');
|
||||
const recentTimestamp = Date.now() - 2 * 60 * 1000; // 2 minutes ago
|
||||
const metadata = createMockMetadata({ registeredAt: recentTimestamp });
|
||||
|
||||
const result = await isDocumentInactive(selfClient, document, metadata);
|
||||
|
||||
expect(result).toBe(false);
|
||||
// Should not call fetch_all or check registration since we short-circuit
|
||||
expect(mockFetchAllPassport).not.toHaveBeenCalled();
|
||||
expect(mockIsUserRegisteredWithAlternativeCSCA).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns false when document was registered exactly 4 minutes ago', async () => {
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('passport');
|
||||
const recentTimestamp = Date.now() - 4 * 60 * 1000; // 4 minutes ago
|
||||
const metadata = createMockMetadata({ registeredAt: recentTimestamp });
|
||||
|
||||
const result = await isDocumentInactive(selfClient, document, metadata);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('continues with full check when document was registered more than 5 minutes ago', async () => {
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('passport', 'test-aki');
|
||||
const oldTimestamp = Date.now() - 10 * 60 * 1000; // 10 minutes ago
|
||||
const metadata = createMockMetadata({ registeredAt: oldTimestamp });
|
||||
|
||||
await isDocumentInactive(selfClient, document, metadata);
|
||||
|
||||
expect(mockFetchAllPassport).toHaveBeenCalled();
|
||||
expect(mockIsUserRegisteredWithAlternativeCSCA).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('continues with full check when registeredAt is undefined', async () => {
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('passport', 'test-aki');
|
||||
const metadata = createMockMetadata({ registeredAt: undefined });
|
||||
|
||||
await isDocumentInactive(selfClient, document, metadata);
|
||||
|
||||
expect(mockFetchAllPassport).toHaveBeenCalled();
|
||||
expect(mockIsUserRegisteredWithAlternativeCSCA).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('secret availability', () => {
|
||||
it('returns true when no secret is available', async () => {
|
||||
mockGetPrivateKey.mockResolvedValue(null);
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('passport', 'test-aki');
|
||||
const metadata = createMockMetadata();
|
||||
|
||||
const result = await isDocumentInactive(selfClient, document, metadata);
|
||||
|
||||
expect(result).toBe(true);
|
||||
// Should still fetch commitment tree
|
||||
expect(mockFetchAllPassport).toHaveBeenCalled();
|
||||
// But should not check registration
|
||||
expect(mockIsUserRegisteredWithAlternativeCSCA).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns true when secret is undefined', async () => {
|
||||
mockGetPrivateKey.mockResolvedValue(undefined);
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('passport', 'test-aki');
|
||||
const metadata = createMockMetadata();
|
||||
|
||||
const result = await isDocumentInactive(selfClient, document, metadata);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when secret is empty string', async () => {
|
||||
mockGetPrivateKey.mockResolvedValue('');
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('passport', 'test-aki');
|
||||
const metadata = createMockMetadata();
|
||||
|
||||
const result = await isDocumentInactive(selfClient, document, metadata);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registration check', () => {
|
||||
it('returns false when document is registered', async () => {
|
||||
mockIsUserRegisteredWithAlternativeCSCA.mockResolvedValue({
|
||||
isRegistered: true,
|
||||
});
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('passport', 'test-aki');
|
||||
const metadata = createMockMetadata();
|
||||
|
||||
const result = await isDocumentInactive(selfClient, document, metadata);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when document is not registered', async () => {
|
||||
mockIsUserRegisteredWithAlternativeCSCA.mockResolvedValue({
|
||||
isRegistered: false,
|
||||
});
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('passport', 'test-aki');
|
||||
const metadata = createMockMetadata();
|
||||
|
||||
const result = await isDocumentInactive(selfClient, document, metadata);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('environment selection', () => {
|
||||
it('uses stg environment for mock documents', async () => {
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('passport', 'test-aki');
|
||||
const metadata = createMockMetadata({ mock: true });
|
||||
|
||||
await isDocumentInactive(selfClient, document, metadata);
|
||||
|
||||
expect(mockFetchAllPassport).toHaveBeenCalledWith('stg', 'test-aki');
|
||||
});
|
||||
|
||||
it('uses prod environment for non-mock documents', async () => {
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('passport', 'test-aki');
|
||||
const metadata = createMockMetadata({ mock: false });
|
||||
|
||||
await isDocumentInactive(selfClient, document, metadata);
|
||||
|
||||
expect(mockFetchAllPassport).toHaveBeenCalledWith('prod', 'test-aki');
|
||||
});
|
||||
});
|
||||
|
||||
describe('document category handling', () => {
|
||||
it('calls passport.fetch_all for passport documents', async () => {
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('passport', 'passport-aki');
|
||||
const metadata = createMockMetadata();
|
||||
|
||||
await isDocumentInactive(selfClient, document, metadata);
|
||||
|
||||
expect(mockFetchAllPassport).toHaveBeenCalledWith('prod', 'passport-aki');
|
||||
expect(mockFetchAllIdCard).not.toHaveBeenCalled();
|
||||
expect(mockFetchAllAadhaar).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls id_card.fetch_all for id_card documents', async () => {
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('id_card', 'id-card-aki');
|
||||
const metadata = createMockMetadata({ documentCategory: 'id_card' });
|
||||
|
||||
await isDocumentInactive(selfClient, document, metadata);
|
||||
|
||||
expect(mockFetchAllIdCard).toHaveBeenCalledWith('prod', 'id-card-aki');
|
||||
expect(mockFetchAllPassport).not.toHaveBeenCalled();
|
||||
expect(mockFetchAllAadhaar).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls aadhaar.fetch_all for aadhaar documents without authorityKeyIdentifier', async () => {
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('aadhaar');
|
||||
const metadata = createMockMetadata({ documentCategory: 'aadhaar' });
|
||||
|
||||
await isDocumentInactive(selfClient, document, metadata);
|
||||
|
||||
expect(mockFetchAllAadhaar).toHaveBeenCalledWith('prod');
|
||||
expect(mockFetchAllPassport).not.toHaveBeenCalled();
|
||||
expect(mockFetchAllIdCard).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses empty string for authorityKeyIdentifier when not present', async () => {
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('passport'); // No AKI
|
||||
const metadata = createMockMetadata();
|
||||
|
||||
await isDocumentInactive(selfClient, document, metadata);
|
||||
|
||||
expect(mockFetchAllPassport).toHaveBeenCalledWith('prod', '');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isUserRegisteredWithAlternativeCSCA integration', () => {
|
||||
it('passes correct arguments to isUserRegisteredWithAlternativeCSCA', async () => {
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('passport', 'test-aki');
|
||||
const metadata = createMockMetadata();
|
||||
|
||||
await isDocumentInactive(selfClient, document, metadata);
|
||||
|
||||
expect(mockIsUserRegisteredWithAlternativeCSCA).toHaveBeenCalledWith(
|
||||
document,
|
||||
'test-secret',
|
||||
expect.objectContaining({
|
||||
getCommitmentTree: expect.any(Function),
|
||||
getAltCSCA: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('getCommitmentTree callback uses getCommitmentTree from stores', async () => {
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('passport', 'test-aki');
|
||||
const metadata = createMockMetadata();
|
||||
|
||||
await isDocumentInactive(selfClient, document, metadata);
|
||||
|
||||
// Get the callbacks passed to isUserRegisteredWithAlternativeCSCA
|
||||
const callbacks =
|
||||
mockIsUserRegisteredWithAlternativeCSCA.mock.calls[0][2];
|
||||
|
||||
// Call the getCommitmentTree callback
|
||||
callbacks.getCommitmentTree('passport');
|
||||
|
||||
expect(mockGetCommitmentTree).toHaveBeenCalledWith(
|
||||
selfClient,
|
||||
'passport',
|
||||
);
|
||||
});
|
||||
|
||||
it('getAltCSCA callback uses getAlternativeCSCA', async () => {
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('passport', 'test-aki');
|
||||
const metadata = createMockMetadata();
|
||||
|
||||
await isDocumentInactive(selfClient, document, metadata);
|
||||
|
||||
// Get the callbacks passed to isUserRegisteredWithAlternativeCSCA
|
||||
const callbacks =
|
||||
mockIsUserRegisteredWithAlternativeCSCA.mock.calls[0][2];
|
||||
|
||||
// Call the getAltCSCA callback
|
||||
callbacks.getAltCSCA('passport');
|
||||
|
||||
expect(mockGetAlternativeCSCA).toHaveBeenCalledWith(
|
||||
selfClient.useProtocolStore,
|
||||
'passport',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('commitment tree caching', () => {
|
||||
it('caches commitment tree fetch for same document category', async () => {
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('passport', 'test-aki');
|
||||
const metadata = createMockMetadata();
|
||||
|
||||
// First call
|
||||
await isDocumentInactive(selfClient, document, metadata);
|
||||
expect(mockFetchAllPassport).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Second call - should use cache
|
||||
await isDocumentInactive(selfClient, document, metadata);
|
||||
expect(mockFetchAllPassport).toHaveBeenCalledTimes(1); // Still 1
|
||||
});
|
||||
|
||||
it('fetches separately for different document categories', async () => {
|
||||
const selfClient = createMockSelfClient();
|
||||
|
||||
// Passport document
|
||||
const passportDoc = createMockDocument('passport', 'passport-aki');
|
||||
const passportMetadata = createMockMetadata();
|
||||
await isDocumentInactive(selfClient, passportDoc, passportMetadata);
|
||||
expect(mockFetchAllPassport).toHaveBeenCalledTimes(1);
|
||||
|
||||
// ID card document - should trigger separate fetch
|
||||
const idCardDoc = createMockDocument('id_card', 'id-card-aki');
|
||||
const idCardMetadata = createMockMetadata({
|
||||
documentCategory: 'id_card',
|
||||
});
|
||||
await isDocumentInactive(selfClient, idCardDoc, idCardMetadata);
|
||||
expect(mockFetchAllIdCard).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Passport shouldn't be fetched again
|
||||
expect(mockFetchAllPassport).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('cache persists across multiple calls for same category', async () => {
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('passport', 'test-aki');
|
||||
const metadata = createMockMetadata();
|
||||
|
||||
// Multiple calls
|
||||
await isDocumentInactive(selfClient, document, metadata);
|
||||
await isDocumentInactive(selfClient, document, metadata);
|
||||
await isDocumentInactive(selfClient, document, metadata);
|
||||
|
||||
// Should only fetch once
|
||||
expect(mockFetchAllPassport).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('propagates errors from getPrivateKey', async () => {
|
||||
mockGetPrivateKey.mockRejectedValue(new Error('Key retrieval failed'));
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('passport', 'test-aki');
|
||||
const metadata = createMockMetadata();
|
||||
|
||||
await expect(
|
||||
isDocumentInactive(selfClient, document, metadata),
|
||||
).rejects.toThrow('Key retrieval failed');
|
||||
});
|
||||
|
||||
it('propagates errors from fetch_all', async () => {
|
||||
mockFetchAllPassport.mockRejectedValue(new Error('Fetch failed'));
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('passport', 'test-aki');
|
||||
const metadata = createMockMetadata();
|
||||
|
||||
await expect(
|
||||
isDocumentInactive(selfClient, document, metadata),
|
||||
).rejects.toThrow('Fetch failed');
|
||||
});
|
||||
|
||||
it('propagates errors from isUserRegisteredWithAlternativeCSCA', async () => {
|
||||
mockIsUserRegisteredWithAlternativeCSCA.mockRejectedValue(
|
||||
new Error('Registration check failed'),
|
||||
);
|
||||
const selfClient = createMockSelfClient();
|
||||
const document = createMockDocument('passport', 'test-aki');
|
||||
const metadata = createMockMetadata();
|
||||
|
||||
await expect(
|
||||
isDocumentInactive(selfClient, document, metadata),
|
||||
).rejects.toThrow('Registration check failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -26,6 +26,8 @@ export const neutral700 = '#404040';
|
||||
|
||||
export const red500 = '#EF4444';
|
||||
|
||||
export const red600 = '#DC2626';
|
||||
|
||||
export const separatorColor = '#E0E0E0';
|
||||
|
||||
export const sky500 = '#0EA5E9';
|
||||
|
||||
Reference in New Issue
Block a user