-
-
Notifications
You must be signed in to change notification settings - Fork 240
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature(locksmith): Natively generate Google Wallet pass for lock's k…
…ey (#13895) * set up necessary google credentials in configuration * set up google wallet pass class and creation operations * google authentication client * operation: google wallet object creation for lock-associated key * add necessary dependencies * set up controller * register native pass generation route * fix typescript error * fix typescript check arror * set dummy google app cred example to improve dev ux * add specific version for jsonwebtoken types dependency * add yarn.lock * Revert "add specific version for jsonwebtoken types dependency" This reverts commit 686f756. * Revert "add yarn.lock" This reverts commit 70f92a1. * update pass controller * improve pass class and object operations for modularity * tests for pass class and object operations * test for pass controller * remove unused import * fix specific jsonwebtoken typed dependency * update yarn.lock
- Loading branch information
Showing
12 changed files
with
721 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import request from 'supertest' | ||
import app from '../../app' | ||
import config from '../../../src/config/config' | ||
import { vi, expect } from 'vitest' | ||
|
||
// Constants defining the parameters for the Google Wallet pass generation | ||
const network = 11155111 | ||
const lockAddress = '0xe6314d3eD5590F2339C57B3011ADC27971D5EadB' | ||
const keyId = '1' | ||
|
||
// Mock configuration specific to Google Wallet API interaction | ||
const mockGoogleConfig = { | ||
googleApplicationCredentials: { | ||
client_email: 'test@example.com', | ||
private_key: 'test_private_key', | ||
}, | ||
googleWalletIssuerID: 'issuer_id', | ||
googleWalletClass: 'wallet_class', | ||
} | ||
|
||
// config with Google Wallet specifics | ||
config.googleApplicationCredentials = | ||
mockGoogleConfig.googleApplicationCredentials | ||
config.googleWalletIssuerID = mockGoogleConfig.googleWalletIssuerID | ||
config.googleWalletClass = mockGoogleConfig.googleWalletClass | ||
|
||
// Mock implementations for dependent services | ||
vi.mock('../../../src/operations/metadataOperations', () => ({ | ||
getLockMetadata: vi.fn(() => ({ | ||
name: 'Test Lock', | ||
description: | ||
'Keys minted from this test lock can be saved to your device as a Google Wallet pass.', | ||
image: | ||
'https://staging-locksmith.unlock-protocol.com/lock/0xe6314d3eD5590F2339C57B3011ADC27971D5EadB/icon', | ||
attributes: [], | ||
external_url: null, | ||
})), | ||
})) | ||
|
||
// Mock for generating QR code URL | ||
vi.mock('../../../src/utils/qrcode', () => ({ | ||
generateQrCodeUrl: vi.fn(() => 'https://example.com/qr'), | ||
})) | ||
|
||
// Mock for ensuring a wallet class exists or creating one | ||
vi.mock( | ||
'../../../src/operations/generate-pass/android/passClassService', | ||
() => ({ | ||
getOrCreateWalletClass: vi.fn(() => 'issuer_id.wallet_class'), | ||
}) | ||
) | ||
|
||
// Mock for creating the wallet pass object | ||
vi.mock( | ||
'../../../src/operations/generate-pass/android/createPassObject', | ||
() => ({ | ||
createWalletPassObject: vi.fn(() => 'https://example.com/pass'), | ||
}) | ||
) | ||
|
||
// tesst suite for Google Wallet pass generation | ||
describe("Generate a Google Wallet pass for a lock's key", () => { | ||
it('should return a response status of 200 and the URL to save the generated wallet pass', async () => { | ||
// number of assertions expected | ||
expect.assertions(2) | ||
|
||
// execute the request to the Google Wallet pass generation endpoint | ||
const generateGoogleWalletPassResponse = await request(app).get( | ||
`/v2/pass/${network}/${lockAddress}/${keyId}/android` | ||
) | ||
|
||
// Assert the HTTP status code to be 200 (OK) | ||
expect(generateGoogleWalletPassResponse.status).toBe(200) | ||
// Assert the response body to contain the pass URL | ||
expect(generateGoogleWalletPassResponse.body).toEqual({ | ||
passObjectUrl: 'https://example.com/pass', | ||
}) | ||
}) | ||
}) |
115 changes: 115 additions & 0 deletions
115
locksmith/__tests__/operations/walletPassClassOperations.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import { describe, it, expect, beforeEach, vi } from 'vitest' | ||
import { googleAuthClient } from '../../src/operations/generate-pass/android/googleAuthClient' | ||
import { | ||
createClass, | ||
getOrCreateWalletClass, | ||
} from '../../src/operations/generate-pass/android/passClassService' | ||
import * as passClassService from '../../src/operations/generate-pass/android/passClassService' | ||
import logger from '../../src/logger' | ||
import { GaxiosResponse, GaxiosError } from 'gaxios' | ||
|
||
// Mock the required modules | ||
vi.mock('../../src/operations/generate-pass/android/googleAuthClient') | ||
vi.mock('../../src/logger') | ||
|
||
describe('Google Wallet Class Operations', () => { | ||
beforeEach(() => { | ||
vi.resetAllMocks() | ||
vi.clearAllMocks() | ||
}) | ||
|
||
describe('createClass', () => { | ||
it('should create a new class and log success', async () => { | ||
const mockClassId = 'testClassId' | ||
const mockResponse: GaxiosResponse = { | ||
data: { success: true }, | ||
status: 200, | ||
statusText: 'OK', | ||
headers: {}, | ||
config: {}, | ||
request: { responseURL: '' }, | ||
} | ||
|
||
vi.mocked(googleAuthClient.request).mockResolvedValueOnce(mockResponse) | ||
|
||
const result = await createClass(mockClassId) | ||
|
||
expect(result).toEqual({ success: true }) | ||
}) | ||
|
||
it('should log and throw an error if class creation fails', async () => { | ||
const mockClassId = 'testClassId' | ||
const mockError = new Error('Request failed') | ||
vi.mocked(googleAuthClient.request).mockRejectedValueOnce(mockError) | ||
|
||
// Ensure createClass returns a promise | ||
await expect(createClass(mockClassId)).rejects.toThrow( | ||
'Error creating class' | ||
) | ||
}) | ||
}) | ||
|
||
describe('getOrCreateWalletClass', () => { | ||
const mockClassId = 'testClassId' | ||
it('should return existing class data if class exists', async () => { | ||
const mockResponse: GaxiosResponse = { | ||
data: { existing: true, id: mockClassId }, | ||
status: 200, | ||
statusText: 'OK', | ||
headers: {}, | ||
config: {}, | ||
request: { responseURL: '' }, | ||
} | ||
|
||
vi.mocked(googleAuthClient.request).mockResolvedValueOnce(mockResponse) | ||
|
||
const result = await getOrCreateWalletClass(mockClassId) | ||
|
||
expect(result).toEqual(mockResponse.data.id) | ||
}) | ||
|
||
it('should log the correct messages and proceed to create a new class when a 404 error occurs', async () => { | ||
// Simulate a 404 error response from Google Wallet API | ||
const mockError: Partial<GaxiosError> = { | ||
response: { | ||
data: null, | ||
status: 404, | ||
statusText: 'Not Found', | ||
headers: {}, | ||
config: {}, | ||
request: { responseURL: '' }, | ||
}, | ||
} | ||
vi.mocked(googleAuthClient.request).mockRejectedValueOnce(mockError) | ||
// Mock the createClass function | ||
const mockCreateClass = vi.fn().mockResolvedValueOnce({ success: true }) | ||
vi.spyOn(passClassService, 'createClass').mockImplementation( | ||
mockCreateClass | ||
) | ||
|
||
// Spy on logger to check if the messages were logged | ||
const loggerInfoSpy = vi.spyOn(logger, 'info') | ||
|
||
// Call getOrCreateWalletClass to test its behavior | ||
try { | ||
await passClassService.getOrCreateWalletClass(mockClassId) | ||
} catch (e) { | ||
// handle | ||
} | ||
|
||
// Verify the logger was called with expected messages | ||
expect(loggerInfoSpy).toHaveBeenCalledWith( | ||
'Class does not exist, creating a new one...' | ||
) | ||
}) | ||
|
||
it('should log and throw an error if there is a problem checking class existence', async () => { | ||
const mockError = new Error('Request failed') | ||
vi.mocked(googleAuthClient.request).mockRejectedValueOnce(mockError) | ||
|
||
await expect(getOrCreateWalletClass(mockClassId)).rejects.toThrow( | ||
'Error checking class existence' | ||
) | ||
}) | ||
}) | ||
}) |
121 changes: 121 additions & 0 deletions
121
locksmith/__tests__/operations/walletPassObjectOperations.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import jwt from 'jsonwebtoken' | ||
import { describe, it, expect, beforeEach, vi } from 'vitest' | ||
import config from '../../config/config' | ||
import { createWalletPassObject } from '../../src/operations/generate-pass/android/createPassObject' | ||
|
||
vi.mock('jsonwebtoken') | ||
vi.mock('../../../config/config') | ||
vi.mock('../../../logger') | ||
|
||
describe('createWalletPassObject', () => { | ||
// define mock configuration | ||
const mockConfig = { | ||
googleApplicationCredentials: { | ||
client_email: 'test@example.com', | ||
private_key: 'test_private_key', | ||
}, | ||
} | ||
|
||
beforeEach(() => { | ||
config.googleApplicationCredentials = | ||
mockConfig.googleApplicationCredentials | ||
// Mock the jwt.sign method to return a fixed token | ||
vi.mocked(jwt.sign).mockImplementation(() => 'mocked_token') | ||
}) | ||
|
||
it('should create a valid save URL', async () => { | ||
// Define input parameters for the createWalletPassObject function | ||
const classId = 'testClassId' | ||
const lockName = 'testLockName' | ||
const networkName = 'testNetworkName' | ||
const lockAddress = 'testLockAddress' | ||
const keyId = 'testKeyId' | ||
const qrCodeUrl = 'https://example.com/qrcode' | ||
|
||
const saveUrl = await createWalletPassObject( | ||
classId, | ||
lockName, | ||
networkName, | ||
lockAddress, | ||
keyId, | ||
qrCodeUrl, | ||
mockConfig.googleApplicationCredentials.client_email, | ||
mockConfig.googleApplicationCredentials.private_key | ||
) | ||
|
||
// Assert that the returned save URL matches the expected URL | ||
expect(saveUrl).toBe(`https://pay.google.com/gp/v/save/mocked_token`) | ||
|
||
// Ensure jwt.sign was called with the correct parameters | ||
expect(jwt.sign).toHaveBeenCalledWith( | ||
{ | ||
iss: mockConfig.googleApplicationCredentials.client_email, | ||
aud: 'google', | ||
origins: [], | ||
typ: 'savetowallet', | ||
payload: { | ||
genericObjects: [ | ||
{ | ||
id: `${classId}.${keyId}`, | ||
classId: classId, | ||
hexBackgroundColor: '#fffcf6', | ||
logo: { | ||
sourceUri: { | ||
uri: 'https://raw.githubusercontent.com/unlock-protocol/unlock/master/design/logo/%C9%84nlock-Logo-monogram-black.png', | ||
}, | ||
contentDescription: { | ||
defaultValue: { | ||
language: 'en-US', | ||
value: 'LOGO_IMAGE_DESCRIPTION', | ||
}, | ||
}, | ||
}, | ||
cardTitle: { | ||
defaultValue: { | ||
language: 'en-US', | ||
value: lockName, | ||
}, | ||
}, | ||
subheader: { | ||
defaultValue: { | ||
language: 'en-US', | ||
value: 'Event', | ||
}, | ||
}, | ||
header: { | ||
defaultValue: { | ||
language: 'en-US', | ||
value: lockName, | ||
}, | ||
}, | ||
textModulesData: [ | ||
{ | ||
id: 'id', | ||
header: 'ID', | ||
body: keyId, | ||
}, | ||
{ | ||
id: 'network', | ||
header: 'Network', | ||
body: networkName, | ||
}, | ||
{ | ||
id: 'lock_address', | ||
header: 'Lock Address', | ||
body: lockAddress, | ||
}, | ||
], | ||
barcode: { | ||
type: 'QR_CODE', | ||
value: qrCodeUrl, | ||
alternateText: '', | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
'test_private_key', | ||
{ algorithm: 'RS256' } | ||
) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.