From 55abe36fe2a17cf8c1c92f1cea228e7543e88cac Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Wed, 11 Aug 2021 15:31:30 -0700 Subject: [PATCH 1/6] Add mockUserToken typing for storage. --- packages/firebase/index.d.ts | 5 ++++- packages/storage-types/index.d.ts | 17 +++++++++++++++-- packages/storage/compat/service.ts | 12 +++++++++--- packages/storage/exp/api.ts | 10 +++++++--- packages/storage/src/service.ts | 6 +++++- 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/packages/firebase/index.d.ts b/packages/firebase/index.d.ts index 18057a17903..2703b5f3447 100644 --- a/packages/firebase/index.d.ts +++ b/packages/firebase/index.d.ts @@ -7850,8 +7850,11 @@ declare namespace firebase.storage { * * @param host - The emulator host (ex: localhost) * @param port - The emulator port (ex: 5001) + * @param options.mockUserToken the mock auth token to use for unit testing Security Rules */ - useEmulator(host: string, port: number): void; + useEmulator(host: string, port: number, options?: { + mockUserToken?: EmulatorMockTokenOptions | string; + }): void; } /** diff --git a/packages/storage-types/index.d.ts b/packages/storage-types/index.d.ts index 735d8c32278..7707043777b 100644 --- a/packages/storage-types/index.d.ts +++ b/packages/storage-types/index.d.ts @@ -16,7 +16,13 @@ */ import { FirebaseApp } from '@firebase/app-types'; -import { CompleteFn, FirebaseError, NextFn, Unsubscribe } from '@firebase/util'; +import { + CompleteFn, + EmulatorMockTokenOptions, + FirebaseError, + NextFn, + Unsubscribe +} from '@firebase/util'; export interface FullMetadata extends UploadMetadata { bucket: string; @@ -135,7 +141,14 @@ export class FirebaseStorage { refFromURL(url: string): Reference; setMaxOperationRetryTime(time: number): void; setMaxUploadRetryTime(time: number): void; - useEmulator(host: string, port: number): void; + + useEmulator( + host: string, + port: number, + options?: { + mockUserToken?: EmulatorMockTokenOptions | string; + } + ): void; } declare module '@firebase/component' { diff --git a/packages/storage/compat/service.ts b/packages/storage/compat/service.ts index 8eeade12257..17a4cb952d4 100644 --- a/packages/storage/compat/service.ts +++ b/packages/storage/compat/service.ts @@ -27,7 +27,7 @@ import { import { ReferenceCompat } from './reference'; import { isUrl, FirebaseStorageImpl } from '../src/service'; import { invalidArgument } from '../src/implementation/error'; -import { Compat } from '@firebase/util'; +import { Compat, EmulatorMockTokenOptions } from '@firebase/util'; /** * A service that provides firebaseStorage.Reference instances. @@ -87,7 +87,13 @@ export class StorageServiceCompat this._delegate.maxOperationRetryTime = time; } - useEmulator(host: string, port: number): void { - connectStorageEmulator(this._delegate, host, port); + useEmulator( + host: string, + port: number, + options: { + mockUserToken?: EmulatorMockTokenOptions | string; + } = {} + ): void { + connectStorageEmulator(this._delegate, host, port, options); } } diff --git a/packages/storage/exp/api.ts b/packages/storage/exp/api.ts index 762c260c72b..80f067fa2f2 100644 --- a/packages/storage/exp/api.ts +++ b/packages/storage/exp/api.ts @@ -50,7 +50,7 @@ import { _getChild as _getChildInternal } from '../src/reference'; import { STORAGE_TYPE } from './constants'; -import { getModularInstance } from '@firebase/util'; +import { EmulatorMockTokenOptions, getModularInstance } from '@firebase/util'; /** * Public types. @@ -307,12 +307,16 @@ export function getStorage( * @param storage - The `StorageService` instance * @param host - The emulator host (ex: localhost) * @param port - The emulator port (ex: 5001) + * @param options.mockUserToken - the mock auth token to use for unit testing Security Rules. * @public */ export function connectStorageEmulator( storage: FirebaseStorage, host: string, - port: number + port: number, + options: { + mockUserToken?: EmulatorMockTokenOptions | string; + } = {} ): void { - connectEmulatorInternal(storage as FirebaseStorageImpl, host, port); + connectEmulatorInternal(storage as FirebaseStorageImpl, host, port, options); } diff --git a/packages/storage/src/service.ts b/packages/storage/src/service.ts index 0a0078c41a4..212484e6836 100644 --- a/packages/storage/src/service.ts +++ b/packages/storage/src/service.ts @@ -39,6 +39,7 @@ import { } from './implementation/error'; import { validateNumber } from './implementation/type'; import { FirebaseStorage } from '../exp/public-types'; +import { EmulatorMockTokenOptions } from '@firebase/util'; export function isUrl(path?: string): boolean { return /^[A-Za-z]+:\/\//.test(path as string); @@ -133,7 +134,10 @@ function extractBucket( export function connectStorageEmulator( storage: FirebaseStorageImpl, host: string, - port: number + port: number, + options: { + mockUserToken?: EmulatorMockTokenOptions | string; + } = {} ): void { storage.host = `http://${host}:${port}`; } From d0abd8ae8c543b2ab45fc74fe78097dd33090bdd Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Wed, 11 Aug 2021 16:35:29 -0700 Subject: [PATCH 2/6] Implement mockUserToken for Storage and fix bugs. --- common/api-review/database.api.md | 2 +- common/api-review/firestore-lite.api.md | 2 +- common/api-review/firestore.api.md | 988 +++++++++--------- common/api-review/storage.api.md | 5 +- packages/database-types/index.d.ts | 17 +- packages/database-types/package.json | 3 + packages/database/src/exp/Database.ts | 10 +- packages/firebase/index.d.ts | 4 +- packages/firestore-types/index.d.ts | 2 +- packages/firestore/src/api/database.ts | 2 +- packages/firestore/src/auth/user.ts | 1 + packages/firestore/src/lite/database.ts | 31 +- .../test/integration/api/validation.test.ts | 26 +- .../firestore/test/unit/api/database.test.ts | 17 +- packages/rules-unit-testing/src/api/index.ts | 1 - packages/storage/src/service.ts | 13 +- .../storage/test/unit/service.compat.test.ts | 66 +- packages/util/src/crypt.ts | 9 + packages/util/src/emulator.ts | 6 +- packages/util/test/emulator.test.ts | 4 +- 20 files changed, 675 insertions(+), 534 deletions(-) diff --git a/common/api-review/database.api.md b/common/api-review/database.api.md index 3fb78e72ac5..04e49d0230b 100644 --- a/common/api-review/database.api.md +++ b/common/api-review/database.api.md @@ -12,7 +12,7 @@ export function child(parent: DatabaseReference, path: string): DatabaseReferenc // @public export function connectDatabaseEmulator(db: Database, host: string, port: number, options?: { - mockUserToken?: EmulatorMockTokenOptions; + mockUserToken?: EmulatorMockTokenOptions | string; }): void; // @public diff --git a/common/api-review/firestore-lite.api.md b/common/api-review/firestore-lite.api.md index 624958d0239..454a4937c5d 100644 --- a/common/api-review/firestore-lite.api.md +++ b/common/api-review/firestore-lite.api.md @@ -51,7 +51,7 @@ export class CollectionReference extends Query { // @public export function connectFirestoreEmulator(firestore: Firestore, host: string, port: number, options?: { - mockUserToken?: EmulatorMockTokenOptions; + mockUserToken?: EmulatorMockTokenOptions | string; }): void; // @public diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index 301c6c2f8fc..1bbddd84bdb 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -1,494 +1,494 @@ -## API Report File for "@firebase/firestore" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts - -import { EmulatorMockTokenOptions } from '@firebase/util'; -import { FirebaseApp } from '@firebase/app-exp'; -import { LogLevelString as LogLevel } from '@firebase/logger'; - -// @public -export function addDoc(reference: CollectionReference, data: T): Promise>; - -// @public -export function arrayRemove(...elements: unknown[]): FieldValue; - -// @public -export function arrayUnion(...elements: unknown[]): FieldValue; - -// @public -export class Bytes { - static fromBase64String(base64: string): Bytes; - static fromUint8Array(array: Uint8Array): Bytes; - isEqual(other: Bytes): boolean; - toBase64(): string; - toString(): string; - toUint8Array(): Uint8Array; -} - -// @public -export const CACHE_SIZE_UNLIMITED = -1; - -// @public -export function clearIndexedDbPersistence(firestore: Firestore): Promise; - -// @public -export function collection(firestore: Firestore, path: string, ...pathSegments: string[]): CollectionReference; - -// @public -export function collection(reference: CollectionReference, path: string, ...pathSegments: string[]): CollectionReference; - -// @public -export function collection(reference: DocumentReference, path: string, ...pathSegments: string[]): CollectionReference; - -// @public -export function collectionGroup(firestore: Firestore, collectionId: string): Query; - -// @public -export class CollectionReference extends Query { - get id(): string; - get parent(): DocumentReference | null; - get path(): string; - readonly type = "collection"; - withConverter(converter: FirestoreDataConverter): CollectionReference; - withConverter(converter: null): CollectionReference; -} - -// @public -export function connectFirestoreEmulator(firestore: Firestore, host: string, port: number, options?: { - mockUserToken?: EmulatorMockTokenOptions; -}): void; - -// @public -export function deleteDoc(reference: DocumentReference): Promise; - -// @public -export function deleteField(): FieldValue; - -// @public -export function disableNetwork(firestore: Firestore): Promise; - -// @public -export function doc(firestore: Firestore, path: string, ...pathSegments: string[]): DocumentReference; - -// @public -export function doc(reference: CollectionReference, path?: string, ...pathSegments: string[]): DocumentReference; - -// @public -export function doc(reference: DocumentReference, path: string, ...pathSegments: string[]): DocumentReference; - -// @public -export interface DocumentChange { - readonly doc: QueryDocumentSnapshot; - readonly newIndex: number; - readonly oldIndex: number; - readonly type: DocumentChangeType; -} - -// @public -export type DocumentChangeType = 'added' | 'removed' | 'modified'; - -// @public -export interface DocumentData { - [field: string]: any; -} - -// @public -export function documentId(): FieldPath; - -// @public -export class DocumentReference { - readonly converter: FirestoreDataConverter | null; - readonly firestore: Firestore; - get id(): string; - get parent(): CollectionReference; - get path(): string; - readonly type = "document"; - withConverter(converter: FirestoreDataConverter): DocumentReference; - withConverter(converter: null): DocumentReference; -} - -// @public -export class DocumentSnapshot { - protected constructor(); - data(options?: SnapshotOptions): T | undefined; - exists(): this is QueryDocumentSnapshot; - get(fieldPath: string | FieldPath, options?: SnapshotOptions): any; - get id(): string; - readonly metadata: SnapshotMetadata; - get ref(): DocumentReference; -} - -// @public -export function enableIndexedDbPersistence(firestore: Firestore, persistenceSettings?: PersistenceSettings): Promise; - -// @public -export function enableMultiTabIndexedDbPersistence(firestore: Firestore): Promise; - -// @public -export function enableNetwork(firestore: Firestore): Promise; - -// @public -export function endAt(snapshot: DocumentSnapshot): QueryConstraint; - -// @public -export function endAt(...fieldValues: unknown[]): QueryConstraint; - -// @public -export function endBefore(snapshot: DocumentSnapshot): QueryConstraint; - -// @public -export function endBefore(...fieldValues: unknown[]): QueryConstraint; - -// @public -export class FieldPath { - constructor(...fieldNames: string[]); - isEqual(other: FieldPath): boolean; -} - -// @public -export abstract class FieldValue { - abstract isEqual(other: FieldValue): boolean; -} - -// @public -export class Firestore { - get app(): FirebaseApp; - toJSON(): object; - type: 'firestore-lite' | 'firestore'; -} - -// @public -export interface FirestoreDataConverter { - fromFirestore(snapshot: QueryDocumentSnapshot, options?: SnapshotOptions): T; - toFirestore(modelObject: T): DocumentData; - toFirestore(modelObject: Partial, options: SetOptions): DocumentData; -} - -// @public -export class FirestoreError extends Error { - readonly code: FirestoreErrorCode; - readonly message: string; - readonly name: string; - readonly stack?: string; -} - -// @public -export type FirestoreErrorCode = 'cancelled' | 'unknown' | 'invalid-argument' | 'deadline-exceeded' | 'not-found' | 'already-exists' | 'permission-denied' | 'resource-exhausted' | 'failed-precondition' | 'aborted' | 'out-of-range' | 'unimplemented' | 'internal' | 'unavailable' | 'data-loss' | 'unauthenticated'; - -// @public -export interface FirestoreSettings { - cacheSizeBytes?: number; - experimentalAutoDetectLongPolling?: boolean; - experimentalForceLongPolling?: boolean; - host?: string; - ignoreUndefinedProperties?: boolean; - ssl?: boolean; -} - -// @public -export class GeoPoint { - constructor(latitude: number, longitude: number); - isEqual(other: GeoPoint): boolean; - get latitude(): number; - get longitude(): number; - toJSON(): { - latitude: number; - longitude: number; - }; -} - -// @public -export function getDoc(reference: DocumentReference): Promise>; - -// @public -export function getDocFromCache(reference: DocumentReference): Promise>; - -// @public -export function getDocFromServer(reference: DocumentReference): Promise>; - -// @public -export function getDocs(query: Query): Promise>; - -// @public -export function getDocsFromCache(query: Query): Promise>; - -// @public -export function getDocsFromServer(query: Query): Promise>; - -// @public -export function getFirestore(app?: FirebaseApp): Firestore; - -// @public -export function increment(n: number): FieldValue; - -// @public -export function initializeFirestore(app: FirebaseApp, settings: FirestoreSettings): Firestore; - -// @public -export function limit(limit: number): QueryConstraint; - -// @public -export function limitToLast(limit: number): QueryConstraint; - -// @public -export function loadBundle(firestore: Firestore, bundleData: ReadableStream | ArrayBuffer | string): LoadBundleTask; - -// @public -export class LoadBundleTask implements PromiseLike { - catch(onRejected: (a: Error) => R | PromiseLike): Promise; - onProgress(next?: (progress: LoadBundleTaskProgress) => unknown, error?: (err: Error) => unknown, complete?: () => void): void; - then(onFulfilled?: (a: LoadBundleTaskProgress) => T | PromiseLike, onRejected?: (a: Error) => R | PromiseLike): Promise; -} - -// @public -export interface LoadBundleTaskProgress { - bytesLoaded: number; - documentsLoaded: number; - taskState: TaskState; - totalBytes: number; - totalDocuments: number; -} - -export { LogLevel } - -// @public -export function namedQuery(firestore: Firestore, name: string): Promise; - -// @public -export function onSnapshot(reference: DocumentReference, observer: { - next?: (snapshot: DocumentSnapshot) => void; - error?: (error: FirestoreError) => void; - complete?: () => void; -}): Unsubscribe; - -// @public -export function onSnapshot(reference: DocumentReference, options: SnapshotListenOptions, observer: { - next?: (snapshot: DocumentSnapshot) => void; - error?: (error: FirestoreError) => void; - complete?: () => void; -}): Unsubscribe; - -// @public -export function onSnapshot(reference: DocumentReference, onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void): Unsubscribe; - -// @public -export function onSnapshot(reference: DocumentReference, options: SnapshotListenOptions, onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void): Unsubscribe; - -// @public -export function onSnapshot(query: Query, observer: { - next?: (snapshot: QuerySnapshot) => void; - error?: (error: FirestoreError) => void; - complete?: () => void; -}): Unsubscribe; - -// @public -export function onSnapshot(query: Query, options: SnapshotListenOptions, observer: { - next?: (snapshot: QuerySnapshot) => void; - error?: (error: FirestoreError) => void; - complete?: () => void; -}): Unsubscribe; - -// @public -export function onSnapshot(query: Query, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void): Unsubscribe; - -// @public -export function onSnapshot(query: Query, options: SnapshotListenOptions, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void): Unsubscribe; - -// @public -export function onSnapshotsInSync(firestore: Firestore, observer: { - next?: (value: void) => void; - error?: (error: FirestoreError) => void; - complete?: () => void; -}): Unsubscribe; - -// @public -export function onSnapshotsInSync(firestore: Firestore, onSync: () => void): Unsubscribe; - -// @public -export function orderBy(fieldPath: string | FieldPath, directionStr?: OrderByDirection): QueryConstraint; - -// @public -export type OrderByDirection = 'desc' | 'asc'; - -// @public -export interface PersistenceSettings { - forceOwnership?: boolean; -} - -// @public -export class Query { - protected constructor(); - readonly converter: FirestoreDataConverter | null; - readonly firestore: Firestore; - readonly type: 'query' | 'collection'; - withConverter(converter: null): Query; - withConverter(converter: FirestoreDataConverter): Query; -} - -// @public -export function query(query: Query, ...queryConstraints: QueryConstraint[]): Query; - -// @public -export abstract class QueryConstraint { - abstract readonly type: QueryConstraintType; -} - -// @public -export type QueryConstraintType = 'where' | 'orderBy' | 'limit' | 'limitToLast' | 'startAt' | 'startAfter' | 'endAt' | 'endBefore'; - -// @public -export class QueryDocumentSnapshot extends DocumentSnapshot { - // @override - data(options?: SnapshotOptions): T; -} - -// @public -export function queryEqual(left: Query, right: Query): boolean; - -// @public -export class QuerySnapshot { - docChanges(options?: SnapshotListenOptions): Array>; - get docs(): Array>; - get empty(): boolean; - forEach(callback: (result: QueryDocumentSnapshot) => void, thisArg?: unknown): void; - readonly metadata: SnapshotMetadata; - readonly query: Query; - get size(): number; -} - -// @public -export function refEqual(left: DocumentReference | CollectionReference, right: DocumentReference | CollectionReference): boolean; - -// @public -export function runTransaction(firestore: Firestore, updateFunction: (transaction: Transaction) => Promise): Promise; - -// @public -export function serverTimestamp(): FieldValue; - -// @public -export function setDoc(reference: DocumentReference, data: T): Promise; - -// @public -export function setDoc(reference: DocumentReference, data: Partial, options: SetOptions): Promise; - -// @public -export function setLogLevel(logLevel: LogLevel): void; - -// @public -export type SetOptions = { - readonly merge?: boolean; -} | { - readonly mergeFields?: Array; -}; - -// @public -export function snapshotEqual(left: DocumentSnapshot | QuerySnapshot, right: DocumentSnapshot | QuerySnapshot): boolean; - -// @public -export interface SnapshotListenOptions { - readonly includeMetadataChanges?: boolean; -} - -// @public -export class SnapshotMetadata { - readonly fromCache: boolean; - readonly hasPendingWrites: boolean; - isEqual(other: SnapshotMetadata): boolean; -} - -// @public -export interface SnapshotOptions { - readonly serverTimestamps?: 'estimate' | 'previous' | 'none'; -} - -// @public -export function startAfter(snapshot: DocumentSnapshot): QueryConstraint; - -// @public -export function startAfter(...fieldValues: unknown[]): QueryConstraint; - -// @public -export function startAt(snapshot: DocumentSnapshot): QueryConstraint; - -// @public -export function startAt(...fieldValues: unknown[]): QueryConstraint; - -// @public -export type TaskState = 'Error' | 'Running' | 'Success'; - -// @public -export function terminate(firestore: Firestore): Promise; - -// @public -export class Timestamp { - constructor( - seconds: number, - nanoseconds: number); - static fromDate(date: Date): Timestamp; - static fromMillis(milliseconds: number): Timestamp; - isEqual(other: Timestamp): boolean; - readonly nanoseconds: number; - static now(): Timestamp; - readonly seconds: number; - toDate(): Date; - toJSON(): { - seconds: number; - nanoseconds: number; - }; - toMillis(): number; - toString(): string; - valueOf(): string; -} - -// @public -export class Transaction { - delete(documentRef: DocumentReference): this; - get(documentRef: DocumentReference): Promise>; - set(documentRef: DocumentReference, data: T): this; - set(documentRef: DocumentReference, data: Partial, options: SetOptions): this; - update(documentRef: DocumentReference, data: UpdateData): this; - update(documentRef: DocumentReference, field: string | FieldPath, value: unknown, ...moreFieldsAndValues: unknown[]): this; -} - -// @public -export interface Unsubscribe { - (): void; -} - -// @public -export interface UpdateData { - [fieldPath: string]: any; -} - -// @public -export function updateDoc(reference: DocumentReference, data: UpdateData): Promise; - -// @public -export function updateDoc(reference: DocumentReference, field: string | FieldPath, value: unknown, ...moreFieldsAndValues: unknown[]): Promise; - -// @public -export function waitForPendingWrites(firestore: Firestore): Promise; - -// @public -export function where(fieldPath: string | FieldPath, opStr: WhereFilterOp, value: unknown): QueryConstraint; - -// @public -export type WhereFilterOp = '<' | '<=' | '==' | '!=' | '>=' | '>' | 'array-contains' | 'in' | 'array-contains-any' | 'not-in'; - -// @public -export class WriteBatch { - commit(): Promise; - delete(documentRef: DocumentReference): WriteBatch; - set(documentRef: DocumentReference, data: T): WriteBatch; - set(documentRef: DocumentReference, data: Partial, options: SetOptions): WriteBatch; - update(documentRef: DocumentReference, data: UpdateData): WriteBatch; - update(documentRef: DocumentReference, field: string | FieldPath, value: unknown, ...moreFieldsAndValues: unknown[]): WriteBatch; -} - -// @public -export function writeBatch(firestore: Firestore): WriteBatch; - - -``` +## API Report File for "@firebase/firestore" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { EmulatorMockTokenOptions } from '@firebase/util'; +import { FirebaseApp } from '@firebase/app-exp'; +import { LogLevelString as LogLevel } from '@firebase/logger'; + +// @public +export function addDoc(reference: CollectionReference, data: T): Promise>; + +// @public +export function arrayRemove(...elements: unknown[]): FieldValue; + +// @public +export function arrayUnion(...elements: unknown[]): FieldValue; + +// @public +export class Bytes { + static fromBase64String(base64: string): Bytes; + static fromUint8Array(array: Uint8Array): Bytes; + isEqual(other: Bytes): boolean; + toBase64(): string; + toString(): string; + toUint8Array(): Uint8Array; +} + +// @public +export const CACHE_SIZE_UNLIMITED = -1; + +// @public +export function clearIndexedDbPersistence(firestore: Firestore): Promise; + +// @public +export function collection(firestore: Firestore, path: string, ...pathSegments: string[]): CollectionReference; + +// @public +export function collection(reference: CollectionReference, path: string, ...pathSegments: string[]): CollectionReference; + +// @public +export function collection(reference: DocumentReference, path: string, ...pathSegments: string[]): CollectionReference; + +// @public +export function collectionGroup(firestore: Firestore, collectionId: string): Query; + +// @public +export class CollectionReference extends Query { + get id(): string; + get parent(): DocumentReference | null; + get path(): string; + readonly type = "collection"; + withConverter(converter: FirestoreDataConverter): CollectionReference; + withConverter(converter: null): CollectionReference; +} + +// @public +export function connectFirestoreEmulator(firestore: Firestore, host: string, port: number, options?: { + mockUserToken?: EmulatorMockTokenOptions | string; +}): void; + +// @public +export function deleteDoc(reference: DocumentReference): Promise; + +// @public +export function deleteField(): FieldValue; + +// @public +export function disableNetwork(firestore: Firestore): Promise; + +// @public +export function doc(firestore: Firestore, path: string, ...pathSegments: string[]): DocumentReference; + +// @public +export function doc(reference: CollectionReference, path?: string, ...pathSegments: string[]): DocumentReference; + +// @public +export function doc(reference: DocumentReference, path: string, ...pathSegments: string[]): DocumentReference; + +// @public +export interface DocumentChange { + readonly doc: QueryDocumentSnapshot; + readonly newIndex: number; + readonly oldIndex: number; + readonly type: DocumentChangeType; +} + +// @public +export type DocumentChangeType = 'added' | 'removed' | 'modified'; + +// @public +export interface DocumentData { + [field: string]: any; +} + +// @public +export function documentId(): FieldPath; + +// @public +export class DocumentReference { + readonly converter: FirestoreDataConverter | null; + readonly firestore: Firestore; + get id(): string; + get parent(): CollectionReference; + get path(): string; + readonly type = "document"; + withConverter(converter: FirestoreDataConverter): DocumentReference; + withConverter(converter: null): DocumentReference; +} + +// @public +export class DocumentSnapshot { + protected constructor(); + data(options?: SnapshotOptions): T | undefined; + exists(): this is QueryDocumentSnapshot; + get(fieldPath: string | FieldPath, options?: SnapshotOptions): any; + get id(): string; + readonly metadata: SnapshotMetadata; + get ref(): DocumentReference; +} + +// @public +export function enableIndexedDbPersistence(firestore: Firestore, persistenceSettings?: PersistenceSettings): Promise; + +// @public +export function enableMultiTabIndexedDbPersistence(firestore: Firestore): Promise; + +// @public +export function enableNetwork(firestore: Firestore): Promise; + +// @public +export function endAt(snapshot: DocumentSnapshot): QueryConstraint; + +// @public +export function endAt(...fieldValues: unknown[]): QueryConstraint; + +// @public +export function endBefore(snapshot: DocumentSnapshot): QueryConstraint; + +// @public +export function endBefore(...fieldValues: unknown[]): QueryConstraint; + +// @public +export class FieldPath { + constructor(...fieldNames: string[]); + isEqual(other: FieldPath): boolean; +} + +// @public +export abstract class FieldValue { + abstract isEqual(other: FieldValue): boolean; +} + +// @public +export class Firestore { + get app(): FirebaseApp; + toJSON(): object; + type: 'firestore-lite' | 'firestore'; +} + +// @public +export interface FirestoreDataConverter { + fromFirestore(snapshot: QueryDocumentSnapshot, options?: SnapshotOptions): T; + toFirestore(modelObject: T): DocumentData; + toFirestore(modelObject: Partial, options: SetOptions): DocumentData; +} + +// @public +export class FirestoreError extends Error { + readonly code: FirestoreErrorCode; + readonly message: string; + readonly name: string; + readonly stack?: string; +} + +// @public +export type FirestoreErrorCode = 'cancelled' | 'unknown' | 'invalid-argument' | 'deadline-exceeded' | 'not-found' | 'already-exists' | 'permission-denied' | 'resource-exhausted' | 'failed-precondition' | 'aborted' | 'out-of-range' | 'unimplemented' | 'internal' | 'unavailable' | 'data-loss' | 'unauthenticated'; + +// @public +export interface FirestoreSettings { + cacheSizeBytes?: number; + experimentalAutoDetectLongPolling?: boolean; + experimentalForceLongPolling?: boolean; + host?: string; + ignoreUndefinedProperties?: boolean; + ssl?: boolean; +} + +// @public +export class GeoPoint { + constructor(latitude: number, longitude: number); + isEqual(other: GeoPoint): boolean; + get latitude(): number; + get longitude(): number; + toJSON(): { + latitude: number; + longitude: number; + }; +} + +// @public +export function getDoc(reference: DocumentReference): Promise>; + +// @public +export function getDocFromCache(reference: DocumentReference): Promise>; + +// @public +export function getDocFromServer(reference: DocumentReference): Promise>; + +// @public +export function getDocs(query: Query): Promise>; + +// @public +export function getDocsFromCache(query: Query): Promise>; + +// @public +export function getDocsFromServer(query: Query): Promise>; + +// @public +export function getFirestore(app?: FirebaseApp): Firestore; + +// @public +export function increment(n: number): FieldValue; + +// @public +export function initializeFirestore(app: FirebaseApp, settings: FirestoreSettings): Firestore; + +// @public +export function limit(limit: number): QueryConstraint; + +// @public +export function limitToLast(limit: number): QueryConstraint; + +// @public +export function loadBundle(firestore: Firestore, bundleData: ReadableStream | ArrayBuffer | string): LoadBundleTask; + +// @public +export class LoadBundleTask implements PromiseLike { + catch(onRejected: (a: Error) => R | PromiseLike): Promise; + onProgress(next?: (progress: LoadBundleTaskProgress) => unknown, error?: (err: Error) => unknown, complete?: () => void): void; + then(onFulfilled?: (a: LoadBundleTaskProgress) => T | PromiseLike, onRejected?: (a: Error) => R | PromiseLike): Promise; +} + +// @public +export interface LoadBundleTaskProgress { + bytesLoaded: number; + documentsLoaded: number; + taskState: TaskState; + totalBytes: number; + totalDocuments: number; +} + +export { LogLevel } + +// @public +export function namedQuery(firestore: Firestore, name: string): Promise; + +// @public +export function onSnapshot(reference: DocumentReference, observer: { + next?: (snapshot: DocumentSnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; +}): Unsubscribe; + +// @public +export function onSnapshot(reference: DocumentReference, options: SnapshotListenOptions, observer: { + next?: (snapshot: DocumentSnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; +}): Unsubscribe; + +// @public +export function onSnapshot(reference: DocumentReference, onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void): Unsubscribe; + +// @public +export function onSnapshot(reference: DocumentReference, options: SnapshotListenOptions, onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void): Unsubscribe; + +// @public +export function onSnapshot(query: Query, observer: { + next?: (snapshot: QuerySnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; +}): Unsubscribe; + +// @public +export function onSnapshot(query: Query, options: SnapshotListenOptions, observer: { + next?: (snapshot: QuerySnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; +}): Unsubscribe; + +// @public +export function onSnapshot(query: Query, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void): Unsubscribe; + +// @public +export function onSnapshot(query: Query, options: SnapshotListenOptions, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void): Unsubscribe; + +// @public +export function onSnapshotsInSync(firestore: Firestore, observer: { + next?: (value: void) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; +}): Unsubscribe; + +// @public +export function onSnapshotsInSync(firestore: Firestore, onSync: () => void): Unsubscribe; + +// @public +export function orderBy(fieldPath: string | FieldPath, directionStr?: OrderByDirection): QueryConstraint; + +// @public +export type OrderByDirection = 'desc' | 'asc'; + +// @public +export interface PersistenceSettings { + forceOwnership?: boolean; +} + +// @public +export class Query { + protected constructor(); + readonly converter: FirestoreDataConverter | null; + readonly firestore: Firestore; + readonly type: 'query' | 'collection'; + withConverter(converter: null): Query; + withConverter(converter: FirestoreDataConverter): Query; +} + +// @public +export function query(query: Query, ...queryConstraints: QueryConstraint[]): Query; + +// @public +export abstract class QueryConstraint { + abstract readonly type: QueryConstraintType; +} + +// @public +export type QueryConstraintType = 'where' | 'orderBy' | 'limit' | 'limitToLast' | 'startAt' | 'startAfter' | 'endAt' | 'endBefore'; + +// @public +export class QueryDocumentSnapshot extends DocumentSnapshot { + // @override + data(options?: SnapshotOptions): T; +} + +// @public +export function queryEqual(left: Query, right: Query): boolean; + +// @public +export class QuerySnapshot { + docChanges(options?: SnapshotListenOptions): Array>; + get docs(): Array>; + get empty(): boolean; + forEach(callback: (result: QueryDocumentSnapshot) => void, thisArg?: unknown): void; + readonly metadata: SnapshotMetadata; + readonly query: Query; + get size(): number; +} + +// @public +export function refEqual(left: DocumentReference | CollectionReference, right: DocumentReference | CollectionReference): boolean; + +// @public +export function runTransaction(firestore: Firestore, updateFunction: (transaction: Transaction) => Promise): Promise; + +// @public +export function serverTimestamp(): FieldValue; + +// @public +export function setDoc(reference: DocumentReference, data: T): Promise; + +// @public +export function setDoc(reference: DocumentReference, data: Partial, options: SetOptions): Promise; + +// @public +export function setLogLevel(logLevel: LogLevel): void; + +// @public +export type SetOptions = { + readonly merge?: boolean; +} | { + readonly mergeFields?: Array; +}; + +// @public +export function snapshotEqual(left: DocumentSnapshot | QuerySnapshot, right: DocumentSnapshot | QuerySnapshot): boolean; + +// @public +export interface SnapshotListenOptions { + readonly includeMetadataChanges?: boolean; +} + +// @public +export class SnapshotMetadata { + readonly fromCache: boolean; + readonly hasPendingWrites: boolean; + isEqual(other: SnapshotMetadata): boolean; +} + +// @public +export interface SnapshotOptions { + readonly serverTimestamps?: 'estimate' | 'previous' | 'none'; +} + +// @public +export function startAfter(snapshot: DocumentSnapshot): QueryConstraint; + +// @public +export function startAfter(...fieldValues: unknown[]): QueryConstraint; + +// @public +export function startAt(snapshot: DocumentSnapshot): QueryConstraint; + +// @public +export function startAt(...fieldValues: unknown[]): QueryConstraint; + +// @public +export type TaskState = 'Error' | 'Running' | 'Success'; + +// @public +export function terminate(firestore: Firestore): Promise; + +// @public +export class Timestamp { + constructor( + seconds: number, + nanoseconds: number); + static fromDate(date: Date): Timestamp; + static fromMillis(milliseconds: number): Timestamp; + isEqual(other: Timestamp): boolean; + readonly nanoseconds: number; + static now(): Timestamp; + readonly seconds: number; + toDate(): Date; + toJSON(): { + seconds: number; + nanoseconds: number; + }; + toMillis(): number; + toString(): string; + valueOf(): string; +} + +// @public +export class Transaction { + delete(documentRef: DocumentReference): this; + get(documentRef: DocumentReference): Promise>; + set(documentRef: DocumentReference, data: T): this; + set(documentRef: DocumentReference, data: Partial, options: SetOptions): this; + update(documentRef: DocumentReference, data: UpdateData): this; + update(documentRef: DocumentReference, field: string | FieldPath, value: unknown, ...moreFieldsAndValues: unknown[]): this; +} + +// @public +export interface Unsubscribe { + (): void; +} + +// @public +export interface UpdateData { + [fieldPath: string]: any; +} + +// @public +export function updateDoc(reference: DocumentReference, data: UpdateData): Promise; + +// @public +export function updateDoc(reference: DocumentReference, field: string | FieldPath, value: unknown, ...moreFieldsAndValues: unknown[]): Promise; + +// @public +export function waitForPendingWrites(firestore: Firestore): Promise; + +// @public +export function where(fieldPath: string | FieldPath, opStr: WhereFilterOp, value: unknown): QueryConstraint; + +// @public +export type WhereFilterOp = '<' | '<=' | '==' | '!=' | '>=' | '>' | 'array-contains' | 'in' | 'array-contains-any' | 'not-in'; + +// @public +export class WriteBatch { + commit(): Promise; + delete(documentRef: DocumentReference): WriteBatch; + set(documentRef: DocumentReference, data: T): WriteBatch; + set(documentRef: DocumentReference, data: Partial, options: SetOptions): WriteBatch; + update(documentRef: DocumentReference, data: UpdateData): WriteBatch; + update(documentRef: DocumentReference, field: string | FieldPath, value: unknown, ...moreFieldsAndValues: unknown[]): WriteBatch; +} + +// @public +export function writeBatch(firestore: Firestore): WriteBatch; + + +``` diff --git a/common/api-review/storage.api.md b/common/api-review/storage.api.md index 5cae77227bc..92906ced407 100644 --- a/common/api-review/storage.api.md +++ b/common/api-review/storage.api.md @@ -6,6 +6,7 @@ import { AppCheckInternalComponentName } from '@firebase/app-check-interop-types'; import { CompleteFn } from '@firebase/util'; +import { EmulatorMockTokenOptions } from '@firebase/util'; import { FirebaseApp } from '@firebase/app'; import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; import { FirebaseError } from '@firebase/util'; @@ -16,7 +17,9 @@ import { Subscribe } from '@firebase/util'; import { Unsubscribe } from '@firebase/util'; // @public -export function connectStorageEmulator(storage: FirebaseStorage, host: string, port: number): void; +export function connectStorageEmulator(storage: FirebaseStorage, host: string, port: number, options?: { + mockUserToken?: EmulatorMockTokenOptions | string; +}): void; // @public export function deleteObject(ref: StorageReference): Promise; diff --git a/packages/database-types/index.d.ts b/packages/database-types/index.d.ts index 2b659016c95..00b0154a4bf 100644 --- a/packages/database-types/index.d.ts +++ b/packages/database-types/index.d.ts @@ -16,6 +16,7 @@ */ import { FirebaseApp } from '@firebase/app-types'; +import { EmulatorMockTokenOptions } from '@firebase/util'; export interface DataSnapshot { child(path: string): DataSnapshot; @@ -34,7 +35,13 @@ export interface DataSnapshot { export interface Database { app: FirebaseApp; - useEmulator(host: string, port: number): void; + useEmulator( + host: string, + port: number, + options?: { + mockUserToken?: EmulatorMockTokenOptions | string; + } + ): void; goOffline(): void; goOnline(): void; ref(path?: string | Reference): Reference; @@ -44,7 +51,13 @@ export interface Database { export class FirebaseDatabase implements Database { private constructor(); app: FirebaseApp; - useEmulator(host: string, port: number): void; + useEmulator( + host: string, + port: number, + options?: { + mockUserToken?: EmulatorMockTokenOptions | string; + } + ): void; goOffline(): void; goOnline(): void; ref(path?: string | Reference): Reference; diff --git a/packages/database-types/package.json b/packages/database-types/package.json index dc1c71809ce..ed1c8e36831 100644 --- a/packages/database-types/package.json +++ b/packages/database-types/package.json @@ -14,6 +14,9 @@ "dependencies": { "@firebase/app-types": "0.6.3" }, + "peerDependencies": { + "@firebase/util": "1.x" + }, "repository": { "directory": "packages/database-types", "type": "git", diff --git a/packages/database/src/exp/Database.ts b/packages/database/src/exp/Database.ts index f4081e153c0..152877b46ce 100644 --- a/packages/database/src/exp/Database.ts +++ b/packages/database/src/exp/Database.ts @@ -308,7 +308,7 @@ export function connectDatabaseEmulator( host: string, port: number, options: { - mockUserToken?: EmulatorMockTokenOptions; + mockUserToken?: EmulatorMockTokenOptions | string; } = {} ): void { db = getModularInstance(db); @@ -329,10 +329,10 @@ export function connectDatabaseEmulator( } tokenProvider = new EmulatorTokenProvider(EmulatorTokenProvider.OWNER); } else if (options.mockUserToken) { - const token = createMockUserToken( - options.mockUserToken, - db.app.options.projectId - ); + const token = + typeof options.mockUserToken === 'string' + ? options.mockUserToken + : createMockUserToken(options.mockUserToken, db.app.options.projectId); tokenProvider = new EmulatorTokenProvider(token); } diff --git a/packages/firebase/index.d.ts b/packages/firebase/index.d.ts index 2703b5f3447..4a8a58dc713 100644 --- a/packages/firebase/index.d.ts +++ b/packages/firebase/index.d.ts @@ -5907,7 +5907,7 @@ declare namespace firebase.database { host: string, port: number, options?: { - mockUserToken?: EmulatorMockTokenOptions; + mockUserToken?: EmulatorMockTokenOptions | string; } ): void; /** @@ -8385,7 +8385,7 @@ declare namespace firebase.firestore { host: string, port: number, options?: { - mockUserToken?: EmulatorMockTokenOptions; + mockUserToken?: EmulatorMockTokenOptions | string; } ): void; diff --git a/packages/firestore-types/index.d.ts b/packages/firestore-types/index.d.ts index f7a76e32064..7085d4eaef7 100644 --- a/packages/firestore-types/index.d.ts +++ b/packages/firestore-types/index.d.ts @@ -65,7 +65,7 @@ export class FirebaseFirestore { host: string, port: number, options?: { - mockUserToken?: EmulatorMockTokenOptions; + mockUserToken?: EmulatorMockTokenOptions | string; } ): void; diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index c411534e117..8ead323e3d0 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -244,7 +244,7 @@ export class Firestore host: string, port: number, options: { - mockUserToken?: EmulatorMockTokenOptions; + mockUserToken?: EmulatorMockTokenOptions | string; } = {} ): void { connectFirestoreEmulator(this._delegate, host, port, options); diff --git a/packages/firestore/src/auth/user.ts b/packages/firestore/src/auth/user.ts index c4c2187d646..ddb3e2ac748 100644 --- a/packages/firestore/src/auth/user.ts +++ b/packages/firestore/src/auth/user.ts @@ -27,6 +27,7 @@ export class User { // non-FirebaseAuth providers. static readonly GOOGLE_CREDENTIALS = new User('google-credentials-uid'); static readonly FIRST_PARTY = new User('first-party-uid'); + static readonly MOCK_USER = new User('mock-user'); constructor(readonly uid: string | null) {} diff --git a/packages/firestore/src/lite/database.ts b/packages/firestore/src/lite/database.ts index 3c55aec83f0..94c7f9f609c 100644 --- a/packages/firestore/src/lite/database.ts +++ b/packages/firestore/src/lite/database.ts @@ -238,7 +238,7 @@ export function connectFirestoreEmulator( host: string, port: number, options: { - mockUserToken?: EmulatorMockTokenOptions; + mockUserToken?: EmulatorMockTokenOptions | string; } = {} ): void { firestore = cast(firestore, Firestore); @@ -258,19 +258,30 @@ export function connectFirestoreEmulator( }); if (options.mockUserToken) { - // Let createMockUserToken validate first (catches common mistakes like - // invalid field "uid" and missing field "sub" / "user_id".) - const token = createMockUserToken(options.mockUserToken); - const uid = options.mockUserToken.sub || options.mockUserToken.user_id; - if (!uid) { - throw new FirestoreError( - Code.INVALID_ARGUMENT, - "mockUserToken must contain 'sub' or 'user_id' field!" + let token: string; + let user: User; + if (typeof options.mockUserToken === 'string') { + token = options.mockUserToken; + user = User.MOCK_USER; + } else { + // Let createMockUserToken validate first (catches common mistakes like + // invalid field "uid" and missing field "sub" / "user_id".) + token = createMockUserToken( + options.mockUserToken, + firestore._app?.options.projectId ); + const uid = options.mockUserToken.sub || options.mockUserToken.user_id; + if (!uid) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + "mockUserToken must contain 'sub' or 'user_id' field!" + ); + } + user = new User(uid); } firestore._credentials = new EmulatorCredentialsProvider( - new OAuthToken(token, new User(uid)) + new OAuthToken(token, user) ); } } diff --git a/packages/firestore/test/integration/api/validation.test.ts b/packages/firestore/test/integration/api/validation.test.ts index 345916eb09e..e47578ee1b4 100644 --- a/packages/firestore/test/integration/api/validation.test.ts +++ b/packages/firestore/test/integration/api/validation.test.ts @@ -158,11 +158,27 @@ apiDescribe('Validation:', (persistence: boolean) => { } ); - validationIt(persistence, 'useEmulator can set mockUserToken', () => { - const db = newTestFirestore('test-project'); - // Verify that this doesn't throw. - db.useEmulator('localhost', 9000, { mockUserToken: { sub: 'foo' } }); - }); + validationIt( + persistence, + 'useEmulator can set mockUserToken object', + () => { + const db = newTestFirestore('test-project'); + // Verify that this doesn't throw. + db.useEmulator('localhost', 9000, { mockUserToken: { sub: 'foo' } }); + } + ); + + validationIt( + persistence, + 'useEmulator can set mockUserToken string', + () => { + const db = newTestFirestore('test-project'); + // Verify that this doesn't throw. + db.useEmulator('localhost', 9000, { + mockUserToken: 'my-mock-user-token' + }); + } + ); validationIt( persistence, diff --git a/packages/firestore/test/unit/api/database.test.ts b/packages/firestore/test/unit/api/database.test.ts index 8ba1d406a6d..8ffec48399a 100644 --- a/packages/firestore/test/unit/api/database.test.ts +++ b/packages/firestore/test/unit/api/database.test.ts @@ -18,6 +18,7 @@ import { expect } from 'chai'; import { EmulatorCredentialsProvider } from '../../../src/api/credentials'; +import { User } from '../../../src/auth/user'; import { collectionReference, documentReference, @@ -252,7 +253,7 @@ describe('Settings', () => { expect(db._delegate._getSettings().ssl).to.be.false; }); - it('sets credentials based on mockUserToken', async () => { + it('sets credentials based on mockUserToken object', async () => { // Use a new instance of Firestore in order to configure settings. const db = newTestFirestore(); const mockUserToken = { sub: 'foobar' }; @@ -264,4 +265,18 @@ describe('Settings', () => { expect(token!.type).to.eql('OAuth'); expect(token!.user.uid).to.eql(mockUserToken.sub); }); + + it('sets credentials based on mockUserToken string', async () => { + // Use a new instance of Firestore in order to configure settings. + const db = newTestFirestore(); + db.useEmulator('localhost', 9000, { + mockUserToken: 'my-custom-mock-user-token' + }); + + const credentials = db._delegate._credentials; + expect(credentials).to.be.instanceOf(EmulatorCredentialsProvider); + const token = await credentials.getToken(); + expect(token!.type).to.eql('OAuth'); + expect(token!.user).to.eql(User.MOCK_USER); + }); }); diff --git a/packages/rules-unit-testing/src/api/index.ts b/packages/rules-unit-testing/src/api/index.ts index 9cf12e24af9..4427006ff68 100644 --- a/packages/rules-unit-testing/src/api/index.ts +++ b/packages/rules-unit-testing/src/api/index.ts @@ -24,7 +24,6 @@ import type { app } from 'firebase-admin'; import { _FirebaseApp } from '@firebase/app-types/private'; import { FirebaseAuthInternal } from '@firebase/auth-interop-types'; import * as request from 'request'; -import { base64 } from '@firebase/util'; import { setLogLevel, LogLevel } from '@firebase/logger'; import { Component, ComponentType } from '@firebase/component'; import { base64Encode } from '@firebase/util'; diff --git a/packages/storage/src/service.ts b/packages/storage/src/service.ts index 212484e6836..e2902181d9a 100644 --- a/packages/storage/src/service.ts +++ b/packages/storage/src/service.ts @@ -39,7 +39,7 @@ import { } from './implementation/error'; import { validateNumber } from './implementation/type'; import { FirebaseStorage } from '../exp/public-types'; -import { EmulatorMockTokenOptions } from '@firebase/util'; +import { createMockUserToken, EmulatorMockTokenOptions } from '@firebase/util'; export function isUrl(path?: string): boolean { return /^[A-Za-z]+:\/\//.test(path as string); @@ -140,6 +140,13 @@ export function connectStorageEmulator( } = {} ): void { storage.host = `http://${host}:${port}`; + const { mockUserToken } = options; + if (mockUserToken) { + storage._overrideAuthToken = + typeof mockUserToken === 'string' + ? mockUserToken + : createMockUserToken(mockUserToken, storage.app.options.projectId); + } } /** @@ -161,6 +168,7 @@ export class FirebaseStorageImpl implements FirebaseStorage { private _deleted: boolean = false; private _maxOperationRetryTime: number; private _maxUploadRetryTime: number; + _overrideAuthToken?: string; constructor( /** @@ -243,6 +251,9 @@ export class FirebaseStorageImpl implements FirebaseStorage { } async _getAuthToken(): Promise { + if (this._overrideAuthToken) { + return this._overrideAuthToken; + } const auth = this._authProvider.getImmediate({ optional: true }); if (auth) { const tokenData = await auth.getToken(); diff --git a/packages/storage/test/unit/service.compat.test.ts b/packages/storage/test/unit/service.compat.test.ts index f6796e3de05..2952131bbb7 100644 --- a/packages/storage/test/unit/service.compat.test.ts +++ b/packages/storage/test/unit/service.compat.test.ts @@ -187,7 +187,7 @@ describe('Firebase Storage > Service', () => { }, 'storage/invalid-default-bucket'); }); }); - describe('connectStorageEmulator(service, host, port)', () => { + describe('connectStorageEmulator(service, host, port, options)', () => { it('sets emulator host correctly', done => { function newSend( connection: TestingConnection, @@ -209,9 +209,69 @@ describe('Firebase Storage > Service', () => { testShared.makePool(newSend) ); service.useEmulator('test.host.org', 1234); - expect((service._delegate as FirebaseStorageImpl).host).to.equal( - 'http://test.host.org:1234' + const impl = service._delegate as FirebaseStorageImpl; + expect(impl.host).to.equal('http://test.host.org:1234'); + expect(impl._overrideAuthToken).to.be.undefined; + void service.ref('test.png').getDownloadURL(); + }); + it('sets mock user token string if specified', done => { + const mockUserToken = 'my-mock-user-token'; + function newSend( + connection: TestingConnection, + url: string, + method: string, + body?: ArrayBufferView | Blob | string | null, + headers?: Headers + ): void { + // Expect emulator host to be in url of storage operations requests, + // in this case getDownloadURL. + expect(url).to.match(/^http:\/\/test\.host\.org:1234.+/); + expect(headers?.['Authorization']).to.eql(`Firebase ${mockUserToken}`); + connection.abort(); + done(); + } + const service = makeService( + testShared.fakeApp, + testShared.fakeAuthProvider, + testShared.fakeAppCheckTokenProvider, + testShared.makePool(newSend) + ); + service.useEmulator('test.host.org', 1234, { mockUserToken }); + const impl = service._delegate as FirebaseStorageImpl; + expect(impl.host).to.equal('http://test.host.org:1234'); + expect(impl._overrideAuthToken).to.equal(mockUserToken); + void service.ref('test.png').getDownloadURL(); + }); + it('creates mock user token from object if specified', done => { + let token: string | undefined = undefined; + function newSend( + connection: TestingConnection, + url: string, + method: string, + body?: ArrayBufferView | Blob | string | null, + headers?: Headers + ): void { + // Expect emulator host to be in url of storage operations requests, + // in this case getDownloadURL. + expect(url).to.match(/^http:\/\/test\.host\.org:1234.+/); + expect(headers?.['Authorization']).to.eql(`Firebase ${token}`); + connection.abort(); + done(); + } + const service = makeService( + testShared.fakeApp, + testShared.fakeAuthProvider, + testShared.fakeAppCheckTokenProvider, + testShared.makePool(newSend) ); + service.useEmulator('test.host.org', 1234, { + mockUserToken: { sub: 'alice' } + }); + const impl = service._delegate as FirebaseStorageImpl; + expect(impl.host).to.equal('http://test.host.org:1234'); + token = impl._overrideAuthToken; + // Token should be an unsigned JWT with header { "alg": "none", "type": "JWT" } (base64url): + expect(token).to.match(/^eyJhbGciOiJub25lIiwidHlwZSI6IkpXVCJ9\./); void service.ref('test.png').getDownloadURL(); }); }); diff --git a/packages/util/src/crypt.ts b/packages/util/src/crypt.ts index 56dba121c0c..df1e058756f 100644 --- a/packages/util/src/crypt.ts +++ b/packages/util/src/crypt.ts @@ -341,6 +341,15 @@ export const base64Encode = function (str: string): string { return base64.encodeByteArray(utf8Bytes, true); }; +/** + * URL-safe base64 encoding (without "." padding in the end). + * e.g. Used in JSON Web Token (JWT) parts. + */ +export const base64urlEncodeWithoutPadding = function (str: string): string { + // Use base64url encoding and remove padding in the end (dot characters). + return base64Encode(str).replace(/\./g, ''); +}; + /** * URL-safe base64 decoding * diff --git a/packages/util/src/emulator.ts b/packages/util/src/emulator.ts index 1119a298868..2850b5be378 100644 --- a/packages/util/src/emulator.ts +++ b/packages/util/src/emulator.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { base64 } from './crypt'; +import { base64urlEncodeWithoutPadding } from './crypt'; // Firebase Auth tokens contain snake_case claims following the JWT standard / convention. /* eslint-disable camelcase */ @@ -135,8 +135,8 @@ export function createMockUserToken( // Unsecured JWTs use the empty string as a signature. const signature = ''; return [ - base64.encodeString(JSON.stringify(header), /*webSafe=*/ false), - base64.encodeString(JSON.stringify(payload), /*webSafe=*/ false), + base64urlEncodeWithoutPadding(JSON.stringify(header)), + base64urlEncodeWithoutPadding(JSON.stringify(payload)), signature ].join('.'); } diff --git a/packages/util/test/emulator.test.ts b/packages/util/test/emulator.test.ts index 2f1122dcc9f..34ebefd9203 100644 --- a/packages/util/test/emulator.test.ts +++ b/packages/util/test/emulator.test.ts @@ -28,7 +28,7 @@ describe('createMockUserToken()', () => { const token = createMockUserToken(options, projectId); const claims = JSON.parse( - base64.decodeString(token.split('.')[1], /*webSafe=*/ false) + base64.decodeString(token.split('.')[1], /*webSafe=*/ true) ); // We add an 'iat' field. expect(claims).to.deep.equal({ @@ -50,7 +50,7 @@ describe('createMockUserToken()', () => { const options = { uid: 'alice' }; expect(() => - createMockUserToken((options as unknown) as EmulatorMockTokenOptions) + createMockUserToken(options as unknown as EmulatorMockTokenOptions) ).to.throw( 'The "uid" field is no longer supported by mockUserToken. Please use "sub" instead for Firebase Auth User ID.' ); From 1e826b58893f0358d53822b24be9edfbd23b3da3 Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Wed, 11 Aug 2021 16:44:54 -0700 Subject: [PATCH 3/6] Revert RUT changes. --- packages/rules-unit-testing/src/api/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/rules-unit-testing/src/api/index.ts b/packages/rules-unit-testing/src/api/index.ts index 4427006ff68..51d16209187 100644 --- a/packages/rules-unit-testing/src/api/index.ts +++ b/packages/rules-unit-testing/src/api/index.ts @@ -24,6 +24,7 @@ import type { app } from 'firebase-admin'; import { _FirebaseApp } from '@firebase/app-types/private'; import { FirebaseAuthInternal } from '@firebase/auth-interop-types'; import * as request from 'request'; +import { base64 } from '@firebase/util'; import { setLogLevel, LogLevel } from '@firebase/logger'; import { Component, ComponentType } from '@firebase/component'; import { base64Encode } from '@firebase/util'; @@ -159,7 +160,7 @@ export type FirebaseEmulatorOptions = { function trimmedBase64Encode(val: string): string { // Use base64url encoding and remove padding in the end (dot characters). - return base64Encode(val).replace(/\./g, ''); + return base64Encode(val).replace(/\./g, ""); } function createUnsecuredJwt(token: TokenOptions, projectId?: string): string { @@ -497,7 +498,7 @@ function initializeApp( ComponentType.PRIVATE ); - (app as unknown as _FirebaseApp)._addOrOverwriteComponent( + ((app as unknown) as _FirebaseApp)._addOrOverwriteComponent( mockAuthComponent ); } @@ -702,7 +703,7 @@ export function assertFails(pr: Promise): any { errCode === 'permission-denied' || errCode === 'permission_denied' || errMessage.indexOf('permission_denied') >= 0 || - errMessage.indexOf('permission denied') >= 0 || + errMessage.indexOf('permission denied') >= 0 || // Storage permission errors contain message: (storage/unauthorized) errMessage.indexOf('unauthorized') >= 0; From c74df54d6ef89213c90e44f259aa1272968e02af Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Wed, 11 Aug 2021 16:45:43 -0700 Subject: [PATCH 4/6] Create wise-toys-care.md --- .changeset/wise-toys-care.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/wise-toys-care.md diff --git a/.changeset/wise-toys-care.md b/.changeset/wise-toys-care.md new file mode 100644 index 00000000000..5463f576316 --- /dev/null +++ b/.changeset/wise-toys-care.md @@ -0,0 +1,12 @@ +--- +"@firebase/database-types": feature +"@firebase/database": feature +"firebase": feature +"@firebase/firestore-types": feature +"@firebase/firestore": feature +"@firebase/storage-types": feature +"@firebase/storage": feature +"@firebase/util": feature +--- + +Implement mockUserToken for Storage and fix JWT format bugs. From 0279432b17beba01182f8e73fb94509ee5de28c2 Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Wed, 11 Aug 2021 16:56:05 -0700 Subject: [PATCH 5/6] Set EOL to unix style. --- common/api-review/firestore.api.md | 988 ++++++++++++++--------------- 1 file changed, 494 insertions(+), 494 deletions(-) diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index 1bbddd84bdb..f650f3d655e 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -1,494 +1,494 @@ -## API Report File for "@firebase/firestore" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts - -import { EmulatorMockTokenOptions } from '@firebase/util'; -import { FirebaseApp } from '@firebase/app-exp'; -import { LogLevelString as LogLevel } from '@firebase/logger'; - -// @public -export function addDoc(reference: CollectionReference, data: T): Promise>; - -// @public -export function arrayRemove(...elements: unknown[]): FieldValue; - -// @public -export function arrayUnion(...elements: unknown[]): FieldValue; - -// @public -export class Bytes { - static fromBase64String(base64: string): Bytes; - static fromUint8Array(array: Uint8Array): Bytes; - isEqual(other: Bytes): boolean; - toBase64(): string; - toString(): string; - toUint8Array(): Uint8Array; -} - -// @public -export const CACHE_SIZE_UNLIMITED = -1; - -// @public -export function clearIndexedDbPersistence(firestore: Firestore): Promise; - -// @public -export function collection(firestore: Firestore, path: string, ...pathSegments: string[]): CollectionReference; - -// @public -export function collection(reference: CollectionReference, path: string, ...pathSegments: string[]): CollectionReference; - -// @public -export function collection(reference: DocumentReference, path: string, ...pathSegments: string[]): CollectionReference; - -// @public -export function collectionGroup(firestore: Firestore, collectionId: string): Query; - -// @public -export class CollectionReference extends Query { - get id(): string; - get parent(): DocumentReference | null; - get path(): string; - readonly type = "collection"; - withConverter(converter: FirestoreDataConverter): CollectionReference; - withConverter(converter: null): CollectionReference; -} - -// @public -export function connectFirestoreEmulator(firestore: Firestore, host: string, port: number, options?: { - mockUserToken?: EmulatorMockTokenOptions | string; -}): void; - -// @public -export function deleteDoc(reference: DocumentReference): Promise; - -// @public -export function deleteField(): FieldValue; - -// @public -export function disableNetwork(firestore: Firestore): Promise; - -// @public -export function doc(firestore: Firestore, path: string, ...pathSegments: string[]): DocumentReference; - -// @public -export function doc(reference: CollectionReference, path?: string, ...pathSegments: string[]): DocumentReference; - -// @public -export function doc(reference: DocumentReference, path: string, ...pathSegments: string[]): DocumentReference; - -// @public -export interface DocumentChange { - readonly doc: QueryDocumentSnapshot; - readonly newIndex: number; - readonly oldIndex: number; - readonly type: DocumentChangeType; -} - -// @public -export type DocumentChangeType = 'added' | 'removed' | 'modified'; - -// @public -export interface DocumentData { - [field: string]: any; -} - -// @public -export function documentId(): FieldPath; - -// @public -export class DocumentReference { - readonly converter: FirestoreDataConverter | null; - readonly firestore: Firestore; - get id(): string; - get parent(): CollectionReference; - get path(): string; - readonly type = "document"; - withConverter(converter: FirestoreDataConverter): DocumentReference; - withConverter(converter: null): DocumentReference; -} - -// @public -export class DocumentSnapshot { - protected constructor(); - data(options?: SnapshotOptions): T | undefined; - exists(): this is QueryDocumentSnapshot; - get(fieldPath: string | FieldPath, options?: SnapshotOptions): any; - get id(): string; - readonly metadata: SnapshotMetadata; - get ref(): DocumentReference; -} - -// @public -export function enableIndexedDbPersistence(firestore: Firestore, persistenceSettings?: PersistenceSettings): Promise; - -// @public -export function enableMultiTabIndexedDbPersistence(firestore: Firestore): Promise; - -// @public -export function enableNetwork(firestore: Firestore): Promise; - -// @public -export function endAt(snapshot: DocumentSnapshot): QueryConstraint; - -// @public -export function endAt(...fieldValues: unknown[]): QueryConstraint; - -// @public -export function endBefore(snapshot: DocumentSnapshot): QueryConstraint; - -// @public -export function endBefore(...fieldValues: unknown[]): QueryConstraint; - -// @public -export class FieldPath { - constructor(...fieldNames: string[]); - isEqual(other: FieldPath): boolean; -} - -// @public -export abstract class FieldValue { - abstract isEqual(other: FieldValue): boolean; -} - -// @public -export class Firestore { - get app(): FirebaseApp; - toJSON(): object; - type: 'firestore-lite' | 'firestore'; -} - -// @public -export interface FirestoreDataConverter { - fromFirestore(snapshot: QueryDocumentSnapshot, options?: SnapshotOptions): T; - toFirestore(modelObject: T): DocumentData; - toFirestore(modelObject: Partial, options: SetOptions): DocumentData; -} - -// @public -export class FirestoreError extends Error { - readonly code: FirestoreErrorCode; - readonly message: string; - readonly name: string; - readonly stack?: string; -} - -// @public -export type FirestoreErrorCode = 'cancelled' | 'unknown' | 'invalid-argument' | 'deadline-exceeded' | 'not-found' | 'already-exists' | 'permission-denied' | 'resource-exhausted' | 'failed-precondition' | 'aborted' | 'out-of-range' | 'unimplemented' | 'internal' | 'unavailable' | 'data-loss' | 'unauthenticated'; - -// @public -export interface FirestoreSettings { - cacheSizeBytes?: number; - experimentalAutoDetectLongPolling?: boolean; - experimentalForceLongPolling?: boolean; - host?: string; - ignoreUndefinedProperties?: boolean; - ssl?: boolean; -} - -// @public -export class GeoPoint { - constructor(latitude: number, longitude: number); - isEqual(other: GeoPoint): boolean; - get latitude(): number; - get longitude(): number; - toJSON(): { - latitude: number; - longitude: number; - }; -} - -// @public -export function getDoc(reference: DocumentReference): Promise>; - -// @public -export function getDocFromCache(reference: DocumentReference): Promise>; - -// @public -export function getDocFromServer(reference: DocumentReference): Promise>; - -// @public -export function getDocs(query: Query): Promise>; - -// @public -export function getDocsFromCache(query: Query): Promise>; - -// @public -export function getDocsFromServer(query: Query): Promise>; - -// @public -export function getFirestore(app?: FirebaseApp): Firestore; - -// @public -export function increment(n: number): FieldValue; - -// @public -export function initializeFirestore(app: FirebaseApp, settings: FirestoreSettings): Firestore; - -// @public -export function limit(limit: number): QueryConstraint; - -// @public -export function limitToLast(limit: number): QueryConstraint; - -// @public -export function loadBundle(firestore: Firestore, bundleData: ReadableStream | ArrayBuffer | string): LoadBundleTask; - -// @public -export class LoadBundleTask implements PromiseLike { - catch(onRejected: (a: Error) => R | PromiseLike): Promise; - onProgress(next?: (progress: LoadBundleTaskProgress) => unknown, error?: (err: Error) => unknown, complete?: () => void): void; - then(onFulfilled?: (a: LoadBundleTaskProgress) => T | PromiseLike, onRejected?: (a: Error) => R | PromiseLike): Promise; -} - -// @public -export interface LoadBundleTaskProgress { - bytesLoaded: number; - documentsLoaded: number; - taskState: TaskState; - totalBytes: number; - totalDocuments: number; -} - -export { LogLevel } - -// @public -export function namedQuery(firestore: Firestore, name: string): Promise; - -// @public -export function onSnapshot(reference: DocumentReference, observer: { - next?: (snapshot: DocumentSnapshot) => void; - error?: (error: FirestoreError) => void; - complete?: () => void; -}): Unsubscribe; - -// @public -export function onSnapshot(reference: DocumentReference, options: SnapshotListenOptions, observer: { - next?: (snapshot: DocumentSnapshot) => void; - error?: (error: FirestoreError) => void; - complete?: () => void; -}): Unsubscribe; - -// @public -export function onSnapshot(reference: DocumentReference, onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void): Unsubscribe; - -// @public -export function onSnapshot(reference: DocumentReference, options: SnapshotListenOptions, onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void): Unsubscribe; - -// @public -export function onSnapshot(query: Query, observer: { - next?: (snapshot: QuerySnapshot) => void; - error?: (error: FirestoreError) => void; - complete?: () => void; -}): Unsubscribe; - -// @public -export function onSnapshot(query: Query, options: SnapshotListenOptions, observer: { - next?: (snapshot: QuerySnapshot) => void; - error?: (error: FirestoreError) => void; - complete?: () => void; -}): Unsubscribe; - -// @public -export function onSnapshot(query: Query, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void): Unsubscribe; - -// @public -export function onSnapshot(query: Query, options: SnapshotListenOptions, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void): Unsubscribe; - -// @public -export function onSnapshotsInSync(firestore: Firestore, observer: { - next?: (value: void) => void; - error?: (error: FirestoreError) => void; - complete?: () => void; -}): Unsubscribe; - -// @public -export function onSnapshotsInSync(firestore: Firestore, onSync: () => void): Unsubscribe; - -// @public -export function orderBy(fieldPath: string | FieldPath, directionStr?: OrderByDirection): QueryConstraint; - -// @public -export type OrderByDirection = 'desc' | 'asc'; - -// @public -export interface PersistenceSettings { - forceOwnership?: boolean; -} - -// @public -export class Query { - protected constructor(); - readonly converter: FirestoreDataConverter | null; - readonly firestore: Firestore; - readonly type: 'query' | 'collection'; - withConverter(converter: null): Query; - withConverter(converter: FirestoreDataConverter): Query; -} - -// @public -export function query(query: Query, ...queryConstraints: QueryConstraint[]): Query; - -// @public -export abstract class QueryConstraint { - abstract readonly type: QueryConstraintType; -} - -// @public -export type QueryConstraintType = 'where' | 'orderBy' | 'limit' | 'limitToLast' | 'startAt' | 'startAfter' | 'endAt' | 'endBefore'; - -// @public -export class QueryDocumentSnapshot extends DocumentSnapshot { - // @override - data(options?: SnapshotOptions): T; -} - -// @public -export function queryEqual(left: Query, right: Query): boolean; - -// @public -export class QuerySnapshot { - docChanges(options?: SnapshotListenOptions): Array>; - get docs(): Array>; - get empty(): boolean; - forEach(callback: (result: QueryDocumentSnapshot) => void, thisArg?: unknown): void; - readonly metadata: SnapshotMetadata; - readonly query: Query; - get size(): number; -} - -// @public -export function refEqual(left: DocumentReference | CollectionReference, right: DocumentReference | CollectionReference): boolean; - -// @public -export function runTransaction(firestore: Firestore, updateFunction: (transaction: Transaction) => Promise): Promise; - -// @public -export function serverTimestamp(): FieldValue; - -// @public -export function setDoc(reference: DocumentReference, data: T): Promise; - -// @public -export function setDoc(reference: DocumentReference, data: Partial, options: SetOptions): Promise; - -// @public -export function setLogLevel(logLevel: LogLevel): void; - -// @public -export type SetOptions = { - readonly merge?: boolean; -} | { - readonly mergeFields?: Array; -}; - -// @public -export function snapshotEqual(left: DocumentSnapshot | QuerySnapshot, right: DocumentSnapshot | QuerySnapshot): boolean; - -// @public -export interface SnapshotListenOptions { - readonly includeMetadataChanges?: boolean; -} - -// @public -export class SnapshotMetadata { - readonly fromCache: boolean; - readonly hasPendingWrites: boolean; - isEqual(other: SnapshotMetadata): boolean; -} - -// @public -export interface SnapshotOptions { - readonly serverTimestamps?: 'estimate' | 'previous' | 'none'; -} - -// @public -export function startAfter(snapshot: DocumentSnapshot): QueryConstraint; - -// @public -export function startAfter(...fieldValues: unknown[]): QueryConstraint; - -// @public -export function startAt(snapshot: DocumentSnapshot): QueryConstraint; - -// @public -export function startAt(...fieldValues: unknown[]): QueryConstraint; - -// @public -export type TaskState = 'Error' | 'Running' | 'Success'; - -// @public -export function terminate(firestore: Firestore): Promise; - -// @public -export class Timestamp { - constructor( - seconds: number, - nanoseconds: number); - static fromDate(date: Date): Timestamp; - static fromMillis(milliseconds: number): Timestamp; - isEqual(other: Timestamp): boolean; - readonly nanoseconds: number; - static now(): Timestamp; - readonly seconds: number; - toDate(): Date; - toJSON(): { - seconds: number; - nanoseconds: number; - }; - toMillis(): number; - toString(): string; - valueOf(): string; -} - -// @public -export class Transaction { - delete(documentRef: DocumentReference): this; - get(documentRef: DocumentReference): Promise>; - set(documentRef: DocumentReference, data: T): this; - set(documentRef: DocumentReference, data: Partial, options: SetOptions): this; - update(documentRef: DocumentReference, data: UpdateData): this; - update(documentRef: DocumentReference, field: string | FieldPath, value: unknown, ...moreFieldsAndValues: unknown[]): this; -} - -// @public -export interface Unsubscribe { - (): void; -} - -// @public -export interface UpdateData { - [fieldPath: string]: any; -} - -// @public -export function updateDoc(reference: DocumentReference, data: UpdateData): Promise; - -// @public -export function updateDoc(reference: DocumentReference, field: string | FieldPath, value: unknown, ...moreFieldsAndValues: unknown[]): Promise; - -// @public -export function waitForPendingWrites(firestore: Firestore): Promise; - -// @public -export function where(fieldPath: string | FieldPath, opStr: WhereFilterOp, value: unknown): QueryConstraint; - -// @public -export type WhereFilterOp = '<' | '<=' | '==' | '!=' | '>=' | '>' | 'array-contains' | 'in' | 'array-contains-any' | 'not-in'; - -// @public -export class WriteBatch { - commit(): Promise; - delete(documentRef: DocumentReference): WriteBatch; - set(documentRef: DocumentReference, data: T): WriteBatch; - set(documentRef: DocumentReference, data: Partial, options: SetOptions): WriteBatch; - update(documentRef: DocumentReference, data: UpdateData): WriteBatch; - update(documentRef: DocumentReference, field: string | FieldPath, value: unknown, ...moreFieldsAndValues: unknown[]): WriteBatch; -} - -// @public -export function writeBatch(firestore: Firestore): WriteBatch; - - -``` +## API Report File for "@firebase/firestore" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { EmulatorMockTokenOptions } from '@firebase/util'; +import { FirebaseApp } from '@firebase/app-exp'; +import { LogLevelString as LogLevel } from '@firebase/logger'; + +// @public +export function addDoc(reference: CollectionReference, data: T): Promise>; + +// @public +export function arrayRemove(...elements: unknown[]): FieldValue; + +// @public +export function arrayUnion(...elements: unknown[]): FieldValue; + +// @public +export class Bytes { + static fromBase64String(base64: string): Bytes; + static fromUint8Array(array: Uint8Array): Bytes; + isEqual(other: Bytes): boolean; + toBase64(): string; + toString(): string; + toUint8Array(): Uint8Array; +} + +// @public +export const CACHE_SIZE_UNLIMITED = -1; + +// @public +export function clearIndexedDbPersistence(firestore: Firestore): Promise; + +// @public +export function collection(firestore: Firestore, path: string, ...pathSegments: string[]): CollectionReference; + +// @public +export function collection(reference: CollectionReference, path: string, ...pathSegments: string[]): CollectionReference; + +// @public +export function collection(reference: DocumentReference, path: string, ...pathSegments: string[]): CollectionReference; + +// @public +export function collectionGroup(firestore: Firestore, collectionId: string): Query; + +// @public +export class CollectionReference extends Query { + get id(): string; + get parent(): DocumentReference | null; + get path(): string; + readonly type = "collection"; + withConverter(converter: FirestoreDataConverter): CollectionReference; + withConverter(converter: null): CollectionReference; +} + +// @public +export function connectFirestoreEmulator(firestore: Firestore, host: string, port: number, options?: { + mockUserToken?: EmulatorMockTokenOptions | string; +}): void; + +// @public +export function deleteDoc(reference: DocumentReference): Promise; + +// @public +export function deleteField(): FieldValue; + +// @public +export function disableNetwork(firestore: Firestore): Promise; + +// @public +export function doc(firestore: Firestore, path: string, ...pathSegments: string[]): DocumentReference; + +// @public +export function doc(reference: CollectionReference, path?: string, ...pathSegments: string[]): DocumentReference; + +// @public +export function doc(reference: DocumentReference, path: string, ...pathSegments: string[]): DocumentReference; + +// @public +export interface DocumentChange { + readonly doc: QueryDocumentSnapshot; + readonly newIndex: number; + readonly oldIndex: number; + readonly type: DocumentChangeType; +} + +// @public +export type DocumentChangeType = 'added' | 'removed' | 'modified'; + +// @public +export interface DocumentData { + [field: string]: any; +} + +// @public +export function documentId(): FieldPath; + +// @public +export class DocumentReference { + readonly converter: FirestoreDataConverter | null; + readonly firestore: Firestore; + get id(): string; + get parent(): CollectionReference; + get path(): string; + readonly type = "document"; + withConverter(converter: FirestoreDataConverter): DocumentReference; + withConverter(converter: null): DocumentReference; +} + +// @public +export class DocumentSnapshot { + protected constructor(); + data(options?: SnapshotOptions): T | undefined; + exists(): this is QueryDocumentSnapshot; + get(fieldPath: string | FieldPath, options?: SnapshotOptions): any; + get id(): string; + readonly metadata: SnapshotMetadata; + get ref(): DocumentReference; +} + +// @public +export function enableIndexedDbPersistence(firestore: Firestore, persistenceSettings?: PersistenceSettings): Promise; + +// @public +export function enableMultiTabIndexedDbPersistence(firestore: Firestore): Promise; + +// @public +export function enableNetwork(firestore: Firestore): Promise; + +// @public +export function endAt(snapshot: DocumentSnapshot): QueryConstraint; + +// @public +export function endAt(...fieldValues: unknown[]): QueryConstraint; + +// @public +export function endBefore(snapshot: DocumentSnapshot): QueryConstraint; + +// @public +export function endBefore(...fieldValues: unknown[]): QueryConstraint; + +// @public +export class FieldPath { + constructor(...fieldNames: string[]); + isEqual(other: FieldPath): boolean; +} + +// @public +export abstract class FieldValue { + abstract isEqual(other: FieldValue): boolean; +} + +// @public +export class Firestore { + get app(): FirebaseApp; + toJSON(): object; + type: 'firestore-lite' | 'firestore'; +} + +// @public +export interface FirestoreDataConverter { + fromFirestore(snapshot: QueryDocumentSnapshot, options?: SnapshotOptions): T; + toFirestore(modelObject: T): DocumentData; + toFirestore(modelObject: Partial, options: SetOptions): DocumentData; +} + +// @public +export class FirestoreError extends Error { + readonly code: FirestoreErrorCode; + readonly message: string; + readonly name: string; + readonly stack?: string; +} + +// @public +export type FirestoreErrorCode = 'cancelled' | 'unknown' | 'invalid-argument' | 'deadline-exceeded' | 'not-found' | 'already-exists' | 'permission-denied' | 'resource-exhausted' | 'failed-precondition' | 'aborted' | 'out-of-range' | 'unimplemented' | 'internal' | 'unavailable' | 'data-loss' | 'unauthenticated'; + +// @public +export interface FirestoreSettings { + cacheSizeBytes?: number; + experimentalAutoDetectLongPolling?: boolean; + experimentalForceLongPolling?: boolean; + host?: string; + ignoreUndefinedProperties?: boolean; + ssl?: boolean; +} + +// @public +export class GeoPoint { + constructor(latitude: number, longitude: number); + isEqual(other: GeoPoint): boolean; + get latitude(): number; + get longitude(): number; + toJSON(): { + latitude: number; + longitude: number; + }; +} + +// @public +export function getDoc(reference: DocumentReference): Promise>; + +// @public +export function getDocFromCache(reference: DocumentReference): Promise>; + +// @public +export function getDocFromServer(reference: DocumentReference): Promise>; + +// @public +export function getDocs(query: Query): Promise>; + +// @public +export function getDocsFromCache(query: Query): Promise>; + +// @public +export function getDocsFromServer(query: Query): Promise>; + +// @public +export function getFirestore(app?: FirebaseApp): Firestore; + +// @public +export function increment(n: number): FieldValue; + +// @public +export function initializeFirestore(app: FirebaseApp, settings: FirestoreSettings): Firestore; + +// @public +export function limit(limit: number): QueryConstraint; + +// @public +export function limitToLast(limit: number): QueryConstraint; + +// @public +export function loadBundle(firestore: Firestore, bundleData: ReadableStream | ArrayBuffer | string): LoadBundleTask; + +// @public +export class LoadBundleTask implements PromiseLike { + catch(onRejected: (a: Error) => R | PromiseLike): Promise; + onProgress(next?: (progress: LoadBundleTaskProgress) => unknown, error?: (err: Error) => unknown, complete?: () => void): void; + then(onFulfilled?: (a: LoadBundleTaskProgress) => T | PromiseLike, onRejected?: (a: Error) => R | PromiseLike): Promise; +} + +// @public +export interface LoadBundleTaskProgress { + bytesLoaded: number; + documentsLoaded: number; + taskState: TaskState; + totalBytes: number; + totalDocuments: number; +} + +export { LogLevel } + +// @public +export function namedQuery(firestore: Firestore, name: string): Promise; + +// @public +export function onSnapshot(reference: DocumentReference, observer: { + next?: (snapshot: DocumentSnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; +}): Unsubscribe; + +// @public +export function onSnapshot(reference: DocumentReference, options: SnapshotListenOptions, observer: { + next?: (snapshot: DocumentSnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; +}): Unsubscribe; + +// @public +export function onSnapshot(reference: DocumentReference, onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void): Unsubscribe; + +// @public +export function onSnapshot(reference: DocumentReference, options: SnapshotListenOptions, onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void): Unsubscribe; + +// @public +export function onSnapshot(query: Query, observer: { + next?: (snapshot: QuerySnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; +}): Unsubscribe; + +// @public +export function onSnapshot(query: Query, options: SnapshotListenOptions, observer: { + next?: (snapshot: QuerySnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; +}): Unsubscribe; + +// @public +export function onSnapshot(query: Query, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void): Unsubscribe; + +// @public +export function onSnapshot(query: Query, options: SnapshotListenOptions, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void): Unsubscribe; + +// @public +export function onSnapshotsInSync(firestore: Firestore, observer: { + next?: (value: void) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; +}): Unsubscribe; + +// @public +export function onSnapshotsInSync(firestore: Firestore, onSync: () => void): Unsubscribe; + +// @public +export function orderBy(fieldPath: string | FieldPath, directionStr?: OrderByDirection): QueryConstraint; + +// @public +export type OrderByDirection = 'desc' | 'asc'; + +// @public +export interface PersistenceSettings { + forceOwnership?: boolean; +} + +// @public +export class Query { + protected constructor(); + readonly converter: FirestoreDataConverter | null; + readonly firestore: Firestore; + readonly type: 'query' | 'collection'; + withConverter(converter: null): Query; + withConverter(converter: FirestoreDataConverter): Query; +} + +// @public +export function query(query: Query, ...queryConstraints: QueryConstraint[]): Query; + +// @public +export abstract class QueryConstraint { + abstract readonly type: QueryConstraintType; +} + +// @public +export type QueryConstraintType = 'where' | 'orderBy' | 'limit' | 'limitToLast' | 'startAt' | 'startAfter' | 'endAt' | 'endBefore'; + +// @public +export class QueryDocumentSnapshot extends DocumentSnapshot { + // @override + data(options?: SnapshotOptions): T; +} + +// @public +export function queryEqual(left: Query, right: Query): boolean; + +// @public +export class QuerySnapshot { + docChanges(options?: SnapshotListenOptions): Array>; + get docs(): Array>; + get empty(): boolean; + forEach(callback: (result: QueryDocumentSnapshot) => void, thisArg?: unknown): void; + readonly metadata: SnapshotMetadata; + readonly query: Query; + get size(): number; +} + +// @public +export function refEqual(left: DocumentReference | CollectionReference, right: DocumentReference | CollectionReference): boolean; + +// @public +export function runTransaction(firestore: Firestore, updateFunction: (transaction: Transaction) => Promise): Promise; + +// @public +export function serverTimestamp(): FieldValue; + +// @public +export function setDoc(reference: DocumentReference, data: T): Promise; + +// @public +export function setDoc(reference: DocumentReference, data: Partial, options: SetOptions): Promise; + +// @public +export function setLogLevel(logLevel: LogLevel): void; + +// @public +export type SetOptions = { + readonly merge?: boolean; +} | { + readonly mergeFields?: Array; +}; + +// @public +export function snapshotEqual(left: DocumentSnapshot | QuerySnapshot, right: DocumentSnapshot | QuerySnapshot): boolean; + +// @public +export interface SnapshotListenOptions { + readonly includeMetadataChanges?: boolean; +} + +// @public +export class SnapshotMetadata { + readonly fromCache: boolean; + readonly hasPendingWrites: boolean; + isEqual(other: SnapshotMetadata): boolean; +} + +// @public +export interface SnapshotOptions { + readonly serverTimestamps?: 'estimate' | 'previous' | 'none'; +} + +// @public +export function startAfter(snapshot: DocumentSnapshot): QueryConstraint; + +// @public +export function startAfter(...fieldValues: unknown[]): QueryConstraint; + +// @public +export function startAt(snapshot: DocumentSnapshot): QueryConstraint; + +// @public +export function startAt(...fieldValues: unknown[]): QueryConstraint; + +// @public +export type TaskState = 'Error' | 'Running' | 'Success'; + +// @public +export function terminate(firestore: Firestore): Promise; + +// @public +export class Timestamp { + constructor( + seconds: number, + nanoseconds: number); + static fromDate(date: Date): Timestamp; + static fromMillis(milliseconds: number): Timestamp; + isEqual(other: Timestamp): boolean; + readonly nanoseconds: number; + static now(): Timestamp; + readonly seconds: number; + toDate(): Date; + toJSON(): { + seconds: number; + nanoseconds: number; + }; + toMillis(): number; + toString(): string; + valueOf(): string; +} + +// @public +export class Transaction { + delete(documentRef: DocumentReference): this; + get(documentRef: DocumentReference): Promise>; + set(documentRef: DocumentReference, data: T): this; + set(documentRef: DocumentReference, data: Partial, options: SetOptions): this; + update(documentRef: DocumentReference, data: UpdateData): this; + update(documentRef: DocumentReference, field: string | FieldPath, value: unknown, ...moreFieldsAndValues: unknown[]): this; +} + +// @public +export interface Unsubscribe { + (): void; +} + +// @public +export interface UpdateData { + [fieldPath: string]: any; +} + +// @public +export function updateDoc(reference: DocumentReference, data: UpdateData): Promise; + +// @public +export function updateDoc(reference: DocumentReference, field: string | FieldPath, value: unknown, ...moreFieldsAndValues: unknown[]): Promise; + +// @public +export function waitForPendingWrites(firestore: Firestore): Promise; + +// @public +export function where(fieldPath: string | FieldPath, opStr: WhereFilterOp, value: unknown): QueryConstraint; + +// @public +export type WhereFilterOp = '<' | '<=' | '==' | '!=' | '>=' | '>' | 'array-contains' | 'in' | 'array-contains-any' | 'not-in'; + +// @public +export class WriteBatch { + commit(): Promise; + delete(documentRef: DocumentReference): WriteBatch; + set(documentRef: DocumentReference, data: T): WriteBatch; + set(documentRef: DocumentReference, data: Partial, options: SetOptions): WriteBatch; + update(documentRef: DocumentReference, data: UpdateData): WriteBatch; + update(documentRef: DocumentReference, field: string | FieldPath, value: unknown, ...moreFieldsAndValues: unknown[]): WriteBatch; +} + +// @public +export function writeBatch(firestore: Firestore): WriteBatch; + + +``` From b86fa54c712d9f0df751985eba10c9e509fc227d Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Thu, 12 Aug 2021 17:14:18 -0700 Subject: [PATCH 6/6] Address review feedback. --- .changeset/wise-toys-care.md | 16 ++--- packages/database-types/package.json | 6 +- .../storage/test/unit/service.compat.test.ts | 66 +------------------ .../storage/test/unit/service.exp.test.ts | 61 ++++++++++++++++- 4 files changed, 73 insertions(+), 76 deletions(-) diff --git a/.changeset/wise-toys-care.md b/.changeset/wise-toys-care.md index 5463f576316..e2bb7a68af3 100644 --- a/.changeset/wise-toys-care.md +++ b/.changeset/wise-toys-care.md @@ -1,12 +1,12 @@ --- -"@firebase/database-types": feature -"@firebase/database": feature -"firebase": feature -"@firebase/firestore-types": feature -"@firebase/firestore": feature -"@firebase/storage-types": feature -"@firebase/storage": feature -"@firebase/util": feature +'@firebase/database-types': minor +'@firebase/database': minor +'firebase': minor +'@firebase/firestore-types': minor +'@firebase/firestore': minor +'@firebase/storage-types': minor +'@firebase/storage': minor +'@firebase/util': minor --- Implement mockUserToken for Storage and fix JWT format bugs. diff --git a/packages/database-types/package.json b/packages/database-types/package.json index ed1c8e36831..d822ccba9df 100644 --- a/packages/database-types/package.json +++ b/packages/database-types/package.json @@ -12,10 +12,8 @@ "index.d.ts" ], "dependencies": { - "@firebase/app-types": "0.6.3" - }, - "peerDependencies": { - "@firebase/util": "1.x" + "@firebase/app-types": "0.6.3", + "@firebase/util": "1.2.0" }, "repository": { "directory": "packages/database-types", diff --git a/packages/storage/test/unit/service.compat.test.ts b/packages/storage/test/unit/service.compat.test.ts index 2952131bbb7..f6796e3de05 100644 --- a/packages/storage/test/unit/service.compat.test.ts +++ b/packages/storage/test/unit/service.compat.test.ts @@ -187,7 +187,7 @@ describe('Firebase Storage > Service', () => { }, 'storage/invalid-default-bucket'); }); }); - describe('connectStorageEmulator(service, host, port, options)', () => { + describe('connectStorageEmulator(service, host, port)', () => { it('sets emulator host correctly', done => { function newSend( connection: TestingConnection, @@ -209,69 +209,9 @@ describe('Firebase Storage > Service', () => { testShared.makePool(newSend) ); service.useEmulator('test.host.org', 1234); - const impl = service._delegate as FirebaseStorageImpl; - expect(impl.host).to.equal('http://test.host.org:1234'); - expect(impl._overrideAuthToken).to.be.undefined; - void service.ref('test.png').getDownloadURL(); - }); - it('sets mock user token string if specified', done => { - const mockUserToken = 'my-mock-user-token'; - function newSend( - connection: TestingConnection, - url: string, - method: string, - body?: ArrayBufferView | Blob | string | null, - headers?: Headers - ): void { - // Expect emulator host to be in url of storage operations requests, - // in this case getDownloadURL. - expect(url).to.match(/^http:\/\/test\.host\.org:1234.+/); - expect(headers?.['Authorization']).to.eql(`Firebase ${mockUserToken}`); - connection.abort(); - done(); - } - const service = makeService( - testShared.fakeApp, - testShared.fakeAuthProvider, - testShared.fakeAppCheckTokenProvider, - testShared.makePool(newSend) - ); - service.useEmulator('test.host.org', 1234, { mockUserToken }); - const impl = service._delegate as FirebaseStorageImpl; - expect(impl.host).to.equal('http://test.host.org:1234'); - expect(impl._overrideAuthToken).to.equal(mockUserToken); - void service.ref('test.png').getDownloadURL(); - }); - it('creates mock user token from object if specified', done => { - let token: string | undefined = undefined; - function newSend( - connection: TestingConnection, - url: string, - method: string, - body?: ArrayBufferView | Blob | string | null, - headers?: Headers - ): void { - // Expect emulator host to be in url of storage operations requests, - // in this case getDownloadURL. - expect(url).to.match(/^http:\/\/test\.host\.org:1234.+/); - expect(headers?.['Authorization']).to.eql(`Firebase ${token}`); - connection.abort(); - done(); - } - const service = makeService( - testShared.fakeApp, - testShared.fakeAuthProvider, - testShared.fakeAppCheckTokenProvider, - testShared.makePool(newSend) + expect((service._delegate as FirebaseStorageImpl).host).to.equal( + 'http://test.host.org:1234' ); - service.useEmulator('test.host.org', 1234, { - mockUserToken: { sub: 'alice' } - }); - const impl = service._delegate as FirebaseStorageImpl; - expect(impl.host).to.equal('http://test.host.org:1234'); - token = impl._overrideAuthToken; - // Token should be an unsigned JWT with header { "alg": "none", "type": "JWT" } (base64url): - expect(token).to.match(/^eyJhbGciOiJub25lIiwidHlwZSI6IkpXVCJ9\./); void service.ref('test.png').getDownloadURL(); }); }); diff --git a/packages/storage/test/unit/service.exp.test.ts b/packages/storage/test/unit/service.exp.test.ts index a5f4c143f3c..5129d7d27d8 100644 --- a/packages/storage/test/unit/service.exp.test.ts +++ b/packages/storage/test/unit/service.exp.test.ts @@ -235,7 +235,7 @@ GOOG4-RSA-SHA256` ); }); }); - describe('connectStorageEmulator(service, host, port)', () => { + describe('connectStorageEmulator(service, host, port, options)', () => { it('sets emulator host correctly', done => { function newSend( connection: TestingConnection, @@ -260,6 +260,65 @@ GOOG4-RSA-SHA256` expect(service.host).to.equal('http://test.host.org:1234'); void getDownloadURL(ref(service, 'test.png')); }); + it('sets mock user token string if specified', done => { + const mockUserToken = 'my-mock-user-token'; + function newSend( + connection: TestingConnection, + url: string, + method: string, + body?: ArrayBufferView | Blob | string | null, + headers?: Headers + ): void { + // Expect emulator host to be in url of storage operations requests, + // in this case getDownloadURL. + expect(url).to.match(/^http:\/\/test\.host\.org:1234.+/); + expect(headers?.['Authorization']).to.eql(`Firebase ${mockUserToken}`); + connection.abort(); + done(); + } + const service = new FirebaseStorageImpl( + testShared.fakeApp, + testShared.fakeAuthProvider, + testShared.fakeAppCheckTokenProvider, + testShared.makePool(newSend) + ); + connectStorageEmulator(service, 'test.host.org', 1234, { mockUserToken }); + expect(service.host).to.equal('http://test.host.org:1234'); + expect(service._overrideAuthToken).to.equal(mockUserToken); + void getDownloadURL(ref(service, 'test.png')); + }); + it('creates mock user token from object if specified', done => { + let token: string | undefined = undefined; + function newSend( + connection: TestingConnection, + url: string, + method: string, + body?: ArrayBufferView | Blob | string | null, + headers?: Headers + ): void { + // Expect emulator host to be in url of storage operations requests, + // in this case getDownloadURL. + expect(url).to.match(/^http:\/\/test\.host\.org:1234.+/); + expect(headers?.['Authorization']).to.eql(`Firebase ${token}`); + connection.abort(); + done(); + } + + const service = new FirebaseStorageImpl( + testShared.fakeApp, + testShared.fakeAuthProvider, + testShared.fakeAppCheckTokenProvider, + testShared.makePool(newSend) + ); + connectStorageEmulator(service, 'test.host.org', 1234, { + mockUserToken: { sub: 'alice' } + }); + expect(service.host).to.equal('http://test.host.org:1234'); + token = service._overrideAuthToken; + // Token should be an unsigned JWT with header { "alg": "none", "type": "JWT" } (base64url): + expect(token).to.match(/^eyJhbGciOiJub25lIiwidHlwZSI6IkpXVCJ9\./); + void getDownloadURL(ref(service, 'test.png')); + }); }); describe('ref(service, path)', () => { const service = new FirebaseStorageImpl(