88 lines
3.3 KiB
TypeScript
88 lines
3.3 KiB
TypeScript
import {
|
|
GooglePubSubError,
|
|
parseGooglePlayRtdn,
|
|
validateGoogleOidcClaims,
|
|
} from './google-pubsub.ts'
|
|
|
|
function assert(condition: boolean, message: string): asserts condition {
|
|
if (!condition) throw new Error(message)
|
|
}
|
|
|
|
Deno.test('Google Pub/Sub OIDC claims require exact audience and service identity', () => {
|
|
validateGoogleOidcClaims({
|
|
aud: 'https://project.functions.supabase.co/google-play-rtdn',
|
|
email: 'pubsub-push@example.iam.gserviceaccount.com',
|
|
email_verified: 'true',
|
|
exp: '2000000000',
|
|
iss: 'https://accounts.google.com',
|
|
}, 'https://project.functions.supabase.co/google-play-rtdn',
|
|
'pubsub-push@example.iam.gserviceaccount.com', 1900000000)
|
|
|
|
for (const claims of [
|
|
{ aud: 'wrong', email: 'pubsub-push@example.iam.gserviceaccount.com', email_verified: true, exp: 2000000000, iss: 'https://accounts.google.com' },
|
|
{ aud: 'https://project.functions.supabase.co/google-play-rtdn', email: 'attacker@example.test', email_verified: true, exp: 2000000000, iss: 'https://accounts.google.com' },
|
|
{ aud: 'https://project.functions.supabase.co/google-play-rtdn', email: 'pubsub-push@example.iam.gserviceaccount.com', email_verified: true, exp: 1800000000, iss: 'https://accounts.google.com' },
|
|
]) {
|
|
let error: unknown
|
|
try {
|
|
validateGoogleOidcClaims(
|
|
claims,
|
|
'https://project.functions.supabase.co/google-play-rtdn',
|
|
'pubsub-push@example.iam.gserviceaccount.com',
|
|
1900000000,
|
|
)
|
|
} catch (candidate) {
|
|
error = candidate
|
|
}
|
|
assert(error instanceof GooglePubSubError, 'invalid OIDC claims must fail closed')
|
|
}
|
|
})
|
|
|
|
Deno.test('Google Play RTDN parser accepts only the canonical package and subscription payload', () => {
|
|
const payload = {
|
|
version: '1.0',
|
|
packageName: 'com.d3ro.voice',
|
|
eventTimeMillis: '1787241600000',
|
|
subscriptionNotification: {
|
|
version: '1.0',
|
|
notificationType: 3,
|
|
purchaseToken: 'registered-google-play-token',
|
|
},
|
|
}
|
|
const notification = parseGooglePlayRtdn({
|
|
message: {
|
|
messageId: 'pubsub-message-1',
|
|
data: btoa(JSON.stringify(payload)),
|
|
},
|
|
})
|
|
assert(notification.kind === 'subscription', 'subscription payload must be discriminated')
|
|
assert(notification.packageName === 'com.d3ro.voice', 'package must be canonical')
|
|
assert(notification.notificationType === 3, 'notification type must be preserved')
|
|
assert(notification.purchaseToken === 'registered-google-play-token', 'token must be preserved internally')
|
|
|
|
payload.packageName = 'com.attacker.app'
|
|
let error: unknown
|
|
try {
|
|
parseGooglePlayRtdn({
|
|
message: { messageId: 'pubsub-message-2', data: btoa(JSON.stringify(payload)) },
|
|
})
|
|
} catch (candidate) {
|
|
error = candidate
|
|
}
|
|
assert(error instanceof GooglePubSubError, 'wrong package must fail closed')
|
|
})
|
|
|
|
Deno.test('Google Play RTDN test notification is acknowledged without a purchase token', () => {
|
|
const notification = parseGooglePlayRtdn({
|
|
message: {
|
|
messageId: 'pubsub-test-message',
|
|
data: btoa(JSON.stringify({
|
|
version: '1.0',
|
|
packageName: 'com.d3ro.voice',
|
|
eventTimeMillis: '1787241600000',
|
|
testNotification: { version: '1.0' },
|
|
})),
|
|
},
|
|
})
|
|
assert(notification.kind === 'test', 'test notification must be recognized')
|
|
})
|