Skip to content

Commit

Permalink
feat: test utilities
Browse files Browse the repository at this point in the history
- Added testing utilities for building configured testing instances
- Testing instances can be composed together to setup a specific configuration for tests easily
  • Loading branch information
Derek Burgman committed Jan 27, 2022
1 parent 91dddd9 commit f21f421
Show file tree
Hide file tree
Showing 16 changed files with 723 additions and 49 deletions.
71 changes: 25 additions & 46 deletions packages/dbx-firebase/src/lib/firestore/firestore.spec.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,47 @@
import {
assertFails,
assertSucceeds,
initializeTestEnvironment,
RulesTestEnvironment,
} from "@firebase/rules-unit-testing";

import { Firestore } from '@firebase/firestore';
import { collection, CollectionReference, DocumentReference } from '@firebase/firestore';
import { FirestoreDocument } from './document';
import { FirestoreCollection, makeFirestoreCollection } from './firestore';
import { authorizedTestWithTestItemCollection, TestItem, testItemCollection, TestItemDocument } from "../../test/firebase.context.item";

/**
* Data for a test item in our firestore collection.
*/
export interface TestItem {

}
describe('FirestoreCollection', () => {

export class TestItemDocument implements FirestoreDocument<TestItem> {
authorizedTestWithTestItemCollection((f) => {

constructor(readonly documentRef: DocumentReference<TestItem>) { }
let firestore: Firestore;
let firestoreCollection: FirestoreCollection<TestItem, TestItemDocument>;

}
beforeEach(async () => {
firestore = f.parent.firestore;
firestoreCollection = makeFirestoreCollection({
itemsPerPage: 50,
collection: testItemCollection(firestore),
makeDocument: (x) => new TestItemDocument(x.documentRef)
});
});

export const testItemCollectionPath = 'test';
describe('makeFirestoreCollection()', () => {

export function testItemCollection(firestore: Firestore): CollectionReference<TestItem> {
return collection(firestore, testItemCollectionPath);
}
it('should create a new collection.', () => {

describe('FirestoreCollection', () => {
firestoreCollection = makeFirestoreCollection({
itemsPerPage: 50,
collection: testItemCollection(firestore),
makeDocument: (x) => new TestItemDocument(x.documentRef)
});

let testEnv: RulesTestEnvironment;
let firestore: Firestore;
let firestoreCollection: FirestoreCollection<TestItem, TestItemDocument>;
expect(firestoreCollection).toBeDefined();
});

beforeEach(async () => {
testEnv = await initializeTestEnvironment({
// projectId: "demo-project-1234",
firestore: {
// rules: fs.readFileSync("firestore.rules", "utf8"),
},
});

firestore = testEnv.authenticatedContext('test').firestore() as any as Firestore;
});
describe('testItemCollection', () => {

afterEach(() => {
testEnv.clearFirestore();
});
it('should create a new document', () => {

describe('makeFirestoreCollection()', () => {
// TODO:

it('should create a new collection.', () => {

firestoreCollection = makeFirestoreCollection({
itemsPerPage: 50,
collection: testItemCollection(firestore),
makeDocument: (x) => new TestItemDocument(x.documentRef)
});

expect(firestoreCollection).toBeDefined();
});

});

});
29 changes: 29 additions & 0 deletions packages/dbx-firebase/src/test/firebase.context.item.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { setDoc } from '@firebase/firestore';
import { TestItemCollectionFixture, testWithTestItemFixture } from './firebase.context.item';
import { authorizedFirebase } from './firebase.context';
import { doc, getDoc } from 'firebase/firestore';

describe('testWithTestItemFixture', () => {

const testWrapper = testWithTestItemFixture()(authorizedFirebase);

testWrapper((f: TestItemCollectionFixture) => {

it('should create a document', async () => {

const documentRef = doc(f.instance.testItemCollection);

await setDoc(documentRef, {
test: true
});

const snapshot = await getDoc(documentRef);

expect(snapshot).toBeDefined();
expect(snapshot.exists()).toBe(true);

});

});

});
79 changes: 79 additions & 0 deletions packages/dbx-firebase/src/test/firebase.context.item.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@

import { AbstractWrappedFixtureWithInstance, JestTestWrappedContextFactoryBuilder, instanceWrapJestTestContextFactory } from '@dereekb/util';
import { Firestore, collection, CollectionReference, DocumentReference } from '@firebase/firestore';
import { WithFieldValue, DocumentData, PartialWithFieldValue, SetOptions, QueryDocumentSnapshot, SnapshotOptions } from 'firebase/firestore';
import { FirestoreDocument } from '../lib/firestore';
import { FirebaseTestingContextFixture } from './firebase';
import { authorizedFirebase } from './firebase.context';

// MARK: Test Item
/**
* Data for a test item in our firestore collection.
*/
export interface TestItem {
test?: boolean;
}

export class TestItemDocument implements FirestoreDocument<TestItem> {

constructor(readonly documentRef: DocumentReference<TestItem>) { }

}

export const testItemCollectionPath = 'test';

/**
* A way to build a testItemCollection from a firestore instance.
*
* @param firestore
* @returns
*/
export function testItemCollection(firestore: Firestore): CollectionReference<TestItem> {
return collection(firestore, testItemCollectionPath).withConverter<TestItem>({

// TODO: Change later?

toFirestore(modelObject: WithFieldValue<TestItem>): DocumentData {
return {
test: false
};
},
fromFirestore(snapshot: QueryDocumentSnapshot<DocumentData>, options?: SnapshotOptions): TestItem {
const data = snapshot.data();
const result: TestItem = { test: data['test'] || false };
return result;
}
});
}

// MARK: Test Item Testing Fixture
export class TestItemCollectionFixtureInstance {

readonly testItemCollection = testItemCollection(this.fixture.parent.firestore);

constructor(readonly fixture: TestItemCollectionFixture) { }

}

/**
* Used to expose a CollectionReference to TestItem for simple tests.
*/
export class TestItemCollectionFixture extends AbstractWrappedFixtureWithInstance<TestItemCollectionFixtureInstance, FirebaseTestingContextFixture> { }

export interface TestItemCollectionFirebaseContextConfig { }

export function testWithTestItemFixture(config?: TestItemCollectionFirebaseContextConfig): JestTestWrappedContextFactoryBuilder<TestItemCollectionFixture, FirebaseTestingContextFixture> {
return instanceWrapJestTestContextFactory({
wrapFixture: (fixture) => new TestItemCollectionFixture(fixture),
makeInstance: (wrap) => new TestItemCollectionFixtureInstance(wrap),
teardownInstance: (instance: TestItemCollectionFixtureInstance) => {
// instance.fixture.parent.instance.clearFirestore();
}
// TODO: Utilize config here using the setup/teardown later if needed.
});
}

/**
* Tests within an authorized context.
*/
export const authorizedTestWithTestItemCollection = testWithTestItemFixture()(authorizedFirebase);
29 changes: 29 additions & 0 deletions packages/dbx-firebase/src/test/firebase.context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { JestTestContextFactory } from '@dereekb/util';
import { firebaseTestBuilder, FirebaseTestingContextFixture } from './firebase';

import { getApp } from "firebase/app";
// import { getFunctions, connectFunctionsEmulator } from "firebase/functions";

// const functions = getFunctions(getApp());
// connectFunctionsEmulator(functions, "localhost", 5001);

export const TESTING_AUTHORIZED_FIREBASE_USER_ID = '0';

export type FirebaseTestContextFactory = JestTestContextFactory<FirebaseTestingContextFixture>;

export const authorizedFirebase: FirebaseTestContextFactory = firebaseTestBuilder({
testEnvironment: {
firestore: {
rules: `rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
`
}
},
rulesContext: { userId: TESTING_AUTHORIZED_FIREBASE_USER_ID }
});
88 changes: 88 additions & 0 deletions packages/dbx-firebase/src/test/firebase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Firestore } from '@firebase/firestore';
import {
TestEnvironmentConfig,
initializeTestEnvironment,
RulesTestEnvironment,
RulesTestContext,
TokenOptions,
} from "@firebase/rules-unit-testing";

import { AbstractJestTestContextFixture, jestTestContextBuilder, JestTestContextFactory, Maybe } from "@dereekb/util";

// import { connectFirestoreEmulator } from "firebase/firestore";

export interface FirebaseTestingRulesContextConfig {
userId: string;
tokenOptions?: Maybe<TokenOptions>;
}

export interface FirebaseTestingConfig {
testEnvironment: TestEnvironmentConfig;
rulesContext?: Maybe<FirebaseTestingRulesContextConfig>;
}

export class FirebaseTestInstance {

private readonly _firestore: Firestore = this.rulesTestContext.firestore() as any;

constructor(readonly rulesTestEnvironment: RulesTestEnvironment, readonly rulesTestContext: RulesTestContext) { }

get firestore(): Firestore {
return this._firestore;
}

clearFirestore(): Promise<void> {
return this.rulesTestEnvironment.clearFirestore();
}

// TODO: Add storage

}

export class FirebaseTestingContextFixture extends AbstractJestTestContextFixture<FirebaseTestInstance> {

// MARK: From Instance
get firestore(): Firestore {
return this.instance.firestore;
}

}

/**
* A JestTestContextBuilderFunction for building firebase test context factories.
*
* This can be used to easily build a testing context that sets up RulesTestEnvironment for tests that sets itself up and tears itself down.
*/
export const firebaseTestBuilder = jestTestContextBuilder<FirebaseTestInstance, FirebaseTestingContextFixture, FirebaseTestingConfig>({
buildConfig: (input?: Partial<FirebaseTestingConfig>) => {
const config: FirebaseTestingConfig = {
testEnvironment: input?.testEnvironment ?? {},
rulesContext: input?.rulesContext
};

return config;
},
buildFixture: () => new FirebaseTestingContextFixture(),
setupInstance: async (config) => {
const rulesTestEnv = await initializeTestEnvironment(config.testEnvironment);
const rulesTestContext = rulesTestContextForConfig(rulesTestEnv, config.rulesContext);
return new FirebaseTestInstance(rulesTestEnv, rulesTestContext);
},
teardownInstance: async (instance) => {
await instance.rulesTestEnvironment.cleanup(); // Cleanup
}
});

// MARK: Internal
function rulesTestContextForConfig(rulesTestEnv: RulesTestEnvironment, testingRulesConfig?: Maybe<FirebaseTestingRulesContextConfig>): RulesTestContext {
let rulesTestContext: RulesTestContext;

if (testingRulesConfig != null) {
rulesTestContext = rulesTestEnv.authenticatedContext(testingRulesConfig.userId, testingRulesConfig.tokenOptions ?? undefined);
console.log('Authneticated?: ', rulesTestContext);
} else {
rulesTestContext = rulesTestEnv.unauthenticatedContext();
}

return rulesTestContext;
}
2 changes: 1 addition & 1 deletion packages/dbx-firebase/tsconfig.lib.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
"types": ["jest"]
},
"exclude": ["src/test-setup.ts", "**/*.spec.ts", "**/*.test.ts"],
"include": ["**/*.ts"]
Expand Down
2 changes: 1 addition & 1 deletion packages/dbx-firebase/tsconfig.spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"types": ["jest", "node"]
},
"files": ["src/test-setup.ts"],
"include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"]
"include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts", "src/test/firebase.context.ts"]
}
1 change: 1 addition & 0 deletions packages/util/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './lib';
export * from './test';
7 changes: 7 additions & 0 deletions packages/util/src/lib/context/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Initialized, Destroyable } from '../lifecycle';

// export type UseContext<T>

// export interface Context extends Initialized, Destroyable {}

// export type ContextFactory<T> = () => Context<T>;
Empty file.
2 changes: 2 additions & 0 deletions packages/util/src/test/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './jest';
export * from './jest.wrap';
Loading

0 comments on commit f21f421

Please sign in to comment.