Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tighten eligibility vpn check #2150

Merged
merged 3 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function fingerprintLocationSpoofingBuilder(): IBuilder<FingerprintLocati
export function fingerprintVpnBuilder(): IBuilder<FingerprintVpn> {
return new Builder<FingerprintVpn>().with('data', {
result: faker.datatype.boolean(),
confidence: 'high',
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import {
fingerprintIpInfoBuilder,
fingerprintLocationSpoofingBuilder,
fingerprintUnsealedDataBuilder,
fingerprintVpnBuilder,
} from '@/datasources/locking-api/entities/__tests__/fingerprint-unsealed-data.entity.builder';
import {
FingerprintIpDataSchema,
FingerprintIpInfoSchema,
FingerprintLocationSpoofingSchema,
FingerprintUnsealedDataSchema,
FingerprintVpnSchema,
} from '@/datasources/locking-api/entities/fingerprint-unsealed-data.entity';
import { ZodError } from 'zod';
import { faker } from '@faker-js/faker';

describe('FingerprintUnsealedData schemas', () => {
describe('FingerprintUnsealedDataEntity', () => {
Expand Down Expand Up @@ -158,4 +161,60 @@ describe('FingerprintUnsealedData schemas', () => {
);
});
});

describe('FingerprintVpnSchema', () => {
it('should validate a FingerprintVpnSchema', () => {
const fingerprintVpn = fingerprintVpnBuilder().build();

const result = FingerprintVpnSchema.safeParse(fingerprintVpn);

expect(result.success).toBe(true);
});

it('should allow undefined data, defaulting to null', () => {
const fingerprintVpn = fingerprintVpnBuilder().build();

// @ts-expect-error - inferred types don't allow optional fields
delete fingerprintVpn.data;

const result = FingerprintVpnSchema.safeParse(fingerprintVpn);

expect(result.success && result.data.data).toBe(null);
});

it('should fallback to unknown for an invalid confidence value', () => {
const fingerprintVpn = {
data: {
...fingerprintVpnBuilder().build().data,
confidence: faker.string.sample(),
},
};

const result = FingerprintVpnSchema.safeParse(fingerprintVpn);

expect(result.success && result.data.data?.confidence).toEqual('unknown');
});

it('should not allow non-boolean result', () => {
const fingerprintVpn = fingerprintVpnBuilder().build();

// @ts-expect-error - value is expected to be a boolean
fingerprintVpn.data.result = 'true';

const result =
FingerprintLocationSpoofingSchema.safeParse(fingerprintVpn);

expect(!result.success && result.error).toStrictEqual(
new ZodError([
{
code: 'invalid_type',
expected: 'boolean',
received: 'string',
path: ['data', 'result'],
message: 'Expected boolean, received string',
},
]),
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,18 @@ export const FingerprintIpInfoSchema = z.object({

export type FingerprintIpInfo = z.infer<typeof FingerprintIpInfoSchema>;

export const FingerprintConfidenceLevels = ['low', 'medium', 'high'] as const;

export const FingerprintVpnSchema = z.object({
data: z.object({ result: z.boolean() }).nullish().default(null),
data: z
.object({
result: z.boolean(),
confidence: z
.enum([...FingerprintConfidenceLevels, 'unknown'])
.catch('unknown'),
})
.nullish()
.default(null),
});

export type FingerprintVpn = z.infer<typeof FingerprintVpnSchema>;
Expand Down
104 changes: 100 additions & 4 deletions src/datasources/locking-api/fingerprint-api.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ describe('FingerprintApiService', () => {
locationSpoofing: fingerprintLocationSpoofingBuilder()
.with('data', { result: false })
.build(),
vpn: fingerprintVpnBuilder().with('data', { result: false }).build(),
vpn: fingerprintVpnBuilder()
.with('data', { result: false, confidence: 'high' })
.build(),
})
.build();
(unsealEventsResponse as jest.Mock).mockResolvedValue(unsealedData);
Expand Down Expand Up @@ -93,7 +95,9 @@ describe('FingerprintApiService', () => {
locationSpoofing: fingerprintLocationSpoofingBuilder()
.with('data', { result: false })
.build(),
vpn: fingerprintVpnBuilder().with('data', { result: false }).build(),
vpn: fingerprintVpnBuilder()
.with('data', { result: false, confidence: 'high' })
.build(),
})
.build();
(unsealEventsResponse as jest.Mock).mockResolvedValue(unsealedData);
Expand Down Expand Up @@ -122,7 +126,9 @@ describe('FingerprintApiService', () => {
locationSpoofing: fingerprintLocationSpoofingBuilder()
.with('data', { result: false })
.build(),
vpn: fingerprintVpnBuilder().with('data', { result: true }).build(),
vpn: fingerprintVpnBuilder()
.with('data', { result: true, confidence: 'high' })
.build(),
})
.build();
(unsealEventsResponse as jest.Mock).mockResolvedValue(unsealedData);
Expand All @@ -144,7 +150,9 @@ describe('FingerprintApiService', () => {
locationSpoofing: fingerprintLocationSpoofingBuilder()
.with('data', { result: false })
.build(),
vpn: fingerprintVpnBuilder().with('data', { result: true }).build(),
vpn: fingerprintVpnBuilder()
.with('data', { result: true, confidence: 'high' })
.build(),
})
.build();
(unsealEventsResponse as jest.Mock).mockResolvedValue(unsealedData);
Expand Down Expand Up @@ -203,5 +211,93 @@ describe('FingerprintApiService', () => {
isVpn: vpn.data?.result,
});
});

it('should return isVpn:false for a low confidence score', async () => {
const eligibilityRequest = eligibilityRequestBuilder().build();
const unsealedData = fingerprintUnsealedDataBuilder()
.with('products', {
ipInfo: null,
locationSpoofing: fingerprintLocationSpoofingBuilder().build(),
vpn: fingerprintVpnBuilder()
.with('data', { result: true, confidence: 'low' })
.build(),
})
.build();
(unsealEventsResponse as jest.Mock).mockResolvedValue(unsealedData);

const result = await service.checkEligibility(eligibilityRequest);

expect(result).toEqual({
requestId: eligibilityRequest.requestId,
isAllowed: expect.anything(),
isVpn: false,
});
});

it('should return isVpn:false for a medium confidence score', async () => {
const eligibilityRequest = eligibilityRequestBuilder().build();
const unsealedData = fingerprintUnsealedDataBuilder()
.with('products', {
ipInfo: null,
locationSpoofing: fingerprintLocationSpoofingBuilder().build(),
vpn: fingerprintVpnBuilder()
.with('data', { result: true, confidence: 'medium' })
.build(),
})
.build();
(unsealEventsResponse as jest.Mock).mockResolvedValue(unsealedData);

const result = await service.checkEligibility(eligibilityRequest);

expect(result).toEqual({
requestId: eligibilityRequest.requestId,
isAllowed: expect.anything(),
isVpn: false,
});
});

it('should return isVpn:true for a high confidence score', async () => {
const eligibilityRequest = eligibilityRequestBuilder().build();
const unsealedData = fingerprintUnsealedDataBuilder()
.with('products', {
ipInfo: null,
locationSpoofing: fingerprintLocationSpoofingBuilder().build(),
vpn: fingerprintVpnBuilder()
.with('data', { result: true, confidence: 'high' })
.build(),
})
.build();
(unsealEventsResponse as jest.Mock).mockResolvedValue(unsealedData);

const result = await service.checkEligibility(eligibilityRequest);

expect(result).toEqual({
requestId: eligibilityRequest.requestId,
isAllowed: expect.anything(),
isVpn: true,
});
});

it('should return isVpn:false for an unknown confidence score', async () => {
const eligibilityRequest = eligibilityRequestBuilder().build();
const unsealedData = fingerprintUnsealedDataBuilder()
.with('products', {
ipInfo: null,
locationSpoofing: fingerprintLocationSpoofingBuilder().build(),
vpn: fingerprintVpnBuilder()
.with('data', { result: true, confidence: 'unknown' })
.build(),
})
.build();
(unsealEventsResponse as jest.Mock).mockResolvedValue(unsealedData);

const result = await service.checkEligibility(eligibilityRequest);

expect(result).toEqual({
requestId: eligibilityRequest.requestId,
isAllowed: expect.anything(),
isVpn: false,
});
});
});
});
9 changes: 8 additions & 1 deletion src/datasources/locking-api/fingerprint-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class FingerprintApiService implements IIdentityApi {
return {
requestId,
isAllowed: this.isAllowed(unsealedData),
isVpn: unsealedData.products.vpn?.data?.result === true,
isVpn: this.isVpn(unsealedData),
};
}

Expand Down Expand Up @@ -73,4 +73,11 @@ export class FingerprintApiService implements IIdentityApi {
(code) => code === null || !this.nonEligibleCountryCodes.includes(code),
);
}

private isVpn(unsealedData: FingerprintUnsealedData): boolean {
return (
unsealedData.products.vpn?.data?.result === true &&
unsealedData.products.vpn?.data?.confidence === 'high'
);
}
}