Skip to content

Commit

Permalink
Tighten eligibility vpn check (#2150)
Browse files Browse the repository at this point in the history
* Tighten eligibility vpn check

* Add more unit tests for fingerprint-api service

* Test FingerprintVpnSchema, extract isVpn method
  • Loading branch information
usame-algan authored Nov 22, 2024
1 parent a4cc91f commit 637416c
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 6 deletions.
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'
);
}
}

0 comments on commit 637416c

Please sign in to comment.