diff --git a/docs/modules/gcloud.md b/docs/modules/gcloud.md index cd30be70..2134fd80 100644 --- a/docs/modules/gcloud.md +++ b/docs/modules/gcloud.md @@ -8,16 +8,16 @@ Testcontainers module for the Google Cloud Platform's [Cloud SDK](https://cloud. npm install @testcontainers/gcloud --save-dev ``` +Currently, the module supports `Firestore` emulators in Native mode and Datastore mode. In order to use them, you should use the following classes: -Currently, the module supports `Firestore` emulators. In order to use it, you should use the following classes: - -Class | Container Image --|- -FirestoreEmulatorContainer | [gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators](https://gcr.io/google.com/cloudsdktool/google-cloud-cli) +Mode | Class | Container Image +-|-|- +Native mode | FirestoreEmulatorContainer | [gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators](https://gcr.io/google.com/cloudsdktool/google-cloud-cli) +Datastore mode | DatastoreEmulatorContainer | [gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators](https://gcr.io/google.com/cloudsdktool/google-cloud-cli) ## Examples -### Firestore +### Firestore Native mode [Starting a Firestore Emulator container with the default image](../../packages/modules/gcloud/src/firestore-emulator-container.test.ts) inside_block:firestore4 @@ -26,3 +26,13 @@ FirestoreEmulatorContainer | [gcr.io/google.com/cloudsdktool/google-cloud-cli:em [Starting a Firestore Emulator container with a custom emulator image](../../packages/modules/gcloud/src/firestore-emulator-container.test.ts) inside_block:firestore5 + +### Firestore Datastore mode + + +[Starting a Datastore Emulator container with the default image](../../packages/modules/gcloud/src/datastore-emulator-container.test.ts) inside_block:datastore4 + + + +[Starting a Datastore Emulator container with a custom emulator image](../../packages/modules/gcloud/src/datastore-emulator-container.test.ts) inside_block:datastore5 + diff --git a/package-lock.json b/package-lock.json index 92ca85d7..b636b9d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2501,6 +2501,26 @@ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "dev": true }, + "node_modules/@google-cloud/datastore": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/datastore/-/datastore-9.0.0.tgz", + "integrity": "sha512-ydwPVrAd363bc+zgwJfoJLBbzEKkHP9WfDgzl34ToNEjGlSBeXLgyuDmkxD25h5bg0IK+cUst/a9psbkaALxHQ==", + "dev": true, + "dependencies": { + "@google-cloud/promisify": "^4.0.0", + "arrify": "^2.0.1", + "async-mutex": "^0.5.0", + "concat-stream": "^2.0.0", + "extend": "^3.0.2", + "google-gax": "^4.0.5", + "is": "^3.3.0", + "split-array-stream": "^2.0.0", + "stream-events": "^1.0.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@google-cloud/firestore": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.9.0.tgz", @@ -2545,7 +2565,6 @@ "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", "dev": true, - "optional": true, "engines": { "node": ">=14" } @@ -6374,7 +6393,6 @@ "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", "dev": true, - "optional": true, "engines": { "node": ">=8" } @@ -6412,6 +6430,21 @@ "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==" }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "dev": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/async-mutex/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, "node_modules/async-retry": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", @@ -11476,6 +11509,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", + "dev": true + }, "node_modules/is-string": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", @@ -17107,6 +17146,15 @@ "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", "dev": true }, + "node_modules/split-array-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/split-array-stream/-/split-array-stream-2.0.0.tgz", + "integrity": "sha512-hmMswlVY91WvGMxs0k8MRgq8zb2mSen4FmDNc5AFiTWtrBpdZN6nwD6kROVe4vNL+ywrvbCKsWVCnEd4riELIg==", + "dev": true, + "dependencies": { + "is-stream-ended": "^0.1.4" + } + }, "node_modules/split-ca": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", @@ -19099,6 +19147,7 @@ "testcontainers": "^10.10.4" }, "devDependencies": { + "@google-cloud/datastore": "^9.0.0", "@google-cloud/firestore": "7.9.0", "firebase-admin": "12.2.0" } diff --git a/packages/modules/gcloud/package.json b/packages/modules/gcloud/package.json index d4f825ec..11b75811 100644 --- a/packages/modules/gcloud/package.json +++ b/packages/modules/gcloud/package.json @@ -34,6 +34,7 @@ "testcontainers": "^10.10.4" }, "devDependencies": { + "@google-cloud/datastore": "^9.0.0", "@google-cloud/firestore": "7.9.0", "firebase-admin": "12.2.0" } diff --git a/packages/modules/gcloud/src/datastore-emulator-container.test.ts b/packages/modules/gcloud/src/datastore-emulator-container.test.ts new file mode 100644 index 00000000..b05d4a0f --- /dev/null +++ b/packages/modules/gcloud/src/datastore-emulator-container.test.ts @@ -0,0 +1,45 @@ +import { DatastoreEmulatorContainer, StartedDatastoreEmulatorContainer } from "./datastore-emulator-container"; +import { Datastore } from "@google-cloud/datastore"; + +describe("DatastoreEmulatorContainer", () => { + jest.setTimeout(240_000); + + // datastore4 { + it("should work using default version", async () => { + const datastoreEmulatorContainer = await new DatastoreEmulatorContainer().start(); + + await checkDatastore(datastoreEmulatorContainer); + + await datastoreEmulatorContainer.stop(); + }); + // } + + // datastore5 { + it("should work using version 468.0.0", async () => { + const datastoreEmulatorContainer = await new DatastoreEmulatorContainer( + "gcr.io/google.com/cloudsdktool/google-cloud-cli:468.0.0-emulators" + ).start(); + + await checkDatastore(datastoreEmulatorContainer); + + await datastoreEmulatorContainer.stop(); + }); + + // } + + async function checkDatastore(datastoreEmulatorContainer: StartedDatastoreEmulatorContainer) { + expect(datastoreEmulatorContainer).toBeDefined(); + const testProjectId = "test-project"; + const testKind = "test-kind"; + const testId = "123"; + const databaseConfig = { projectId: testProjectId, apiEndpoint: datastoreEmulatorContainer.getEmulatorEndpoint() }; + const datastore = new Datastore(databaseConfig); + + const key = datastore.key([testKind, testId]); + const data = { message: "Hello, Datastore!" }; + await datastore.save({ key, data }); + const [entity] = await datastore.get(key); + + expect(entity).toEqual({ message: "Hello, Datastore!", [Datastore.KEY]: key }); + } +}); diff --git a/packages/modules/gcloud/src/datastore-emulator-container.ts b/packages/modules/gcloud/src/datastore-emulator-container.ts new file mode 100644 index 00000000..8846c0ab --- /dev/null +++ b/packages/modules/gcloud/src/datastore-emulator-container.ts @@ -0,0 +1,33 @@ +import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; + +const EMULATOR_PORT = 8080; +const CMD = `gcloud beta emulators firestore start --host-port 0.0.0.0:${EMULATOR_PORT} --database-mode=datastore-mode`; +const DEFAULT_IMAGE = "gcr.io/google.com/cloudsdktool/cloud-sdk"; + +export class DatastoreEmulatorContainer extends GenericContainer { + constructor(image = DEFAULT_IMAGE) { + super(image); + this.withExposedPorts(EMULATOR_PORT) + .withCommand(["/bin/sh", "-c", CMD]) + .withWaitStrategy(Wait.forLogMessage(RegExp(".*running.*"), 1)) + .withStartupTimeout(120_000); + } + + public override async start(): Promise { + return new StartedDatastoreEmulatorContainer(await super.start()); + } +} + +export class StartedDatastoreEmulatorContainer extends AbstractStartedContainer { + constructor(startedTestContainer: StartedTestContainer) { + super(startedTestContainer); + } + + /** + * @return a host:port pair corresponding to the address on which the emulator is + * reachable from the test host machine. + */ + public getEmulatorEndpoint(): string { + return `${this.getHost()}:${this.getMappedPort(EMULATOR_PORT)}`; + } +} diff --git a/packages/modules/gcloud/src/index.ts b/packages/modules/gcloud/src/index.ts index 1090650f..e37ee7f6 100644 --- a/packages/modules/gcloud/src/index.ts +++ b/packages/modules/gcloud/src/index.ts @@ -1 +1,2 @@ export { FirestoreEmulatorContainer, StartedFirestoreEmulatorContainer } from "./firestore-emulator-container"; +export { DatastoreEmulatorContainer, StartedDatastoreEmulatorContainer } from "./datastore-emulator-container";