Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion lib/DbBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const {
ALLOWED_REGIONS, STAGE_ENV, STAGE_ENDPOINT, PROD_ENDPOINT_RUNTIME, PROD_ENDPOINT_EXTERNAL
} = require("./constants")
const { getCliEnv } = require("@adobe/aio-lib-env")
const { isProdWorkspace } = require("../utils/runtimeNamespace")

class DbBase {
/**
Expand All @@ -28,7 +29,6 @@ class DbBase {
* @hideconstructor
*/
constructor(region, runtimeNamespace, token) {

this.runtimeNamespace = runtimeNamespace
if (!this.runtimeNamespace) {
throw new DbError('Runtime namespace is required')
Expand All @@ -46,6 +46,10 @@ class DbBase {
throw new DbError(`Invalid region '${region}' for the ${env} environment, must be one of: ${validRegions.join(', ')}`)
}

if (process.env.AIO_DEV && isProdWorkspace(this.runtimeNamespace)) {
throw new DbError('Cannot access production databases when using \'aio app dev\'.')
}

let serviceUrl
// Allow overriding service URL via environment variable for testing
if (process.env.AIO_DB_ENDPOINT) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/aio-lib-db",
"version": "1.0.0",
"version": "1.0.1",
"description": "An abstraction on top of Document DB storage",
"main": "index.js",
"scripts": {
Expand Down
58 changes: 45 additions & 13 deletions tests/lib/DbBase.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,9 @@ describe('DbBase tests', () => {
]

for (const r of allowed) {
await expect(DbBase.init({ region: r, namespace: TEST_NAMESPACE, token: TEST_ACCESS_TOKEN })).resolves
await expect(
DbBase.init({ region: r, namespace: TEST_NAMESPACE, token: TEST_ACCESS_TOKEN })
).resolves.not.toThrow()
}
for (const r of unsupported) {
await expect(
Expand Down Expand Up @@ -190,6 +192,8 @@ describe('DbBase tests', () => {
})

test('uses correct endpoints based on execution context', async () => {
// Make sure to use a non-prod namespace to avoid restriction when AIO_DEV is set
const nonProdNs = `${TEST_NAMESPACE}-dev`
const region = 'amer'
const stageUrl = STAGE_ENDPOINT.replaceAll(/<region>/gi, region)
const runtimeProdUrl = PROD_ENDPOINT_RUNTIME.replaceAll(/<region>/gi, region)
Expand All @@ -201,30 +205,30 @@ describe('DbBase tests', () => {
delete process.env.AIO_DEV

process.env.AIO_DB_ENVIRONMENT = STAGE_ENV
const runtimeStageDb = await DbBase.init({ namespace: TEST_NAMESPACE, token: TEST_ACCESS_TOKEN, region })
const runtimeStageDb = await DbBase.init({ namespace: nonProdNs, token: TEST_ACCESS_TOKEN, region })
expect(runtimeStageDb.serviceUrl).toBe(stageUrl)

process.env.AIO_DB_ENVIRONMENT = PROD_ENV
const runtimeProdDb = await DbBase.init({ namespace: TEST_NAMESPACE, token: TEST_ACCESS_TOKEN, region })
const runtimeProdDb = await DbBase.init({ namespace: nonProdNs, token: TEST_ACCESS_TOKEN, region })
expect(runtimeProdDb.serviceUrl).toBe(runtimeProdUrl)

delete process.env.AIO_DB_ENVIRONMENT
const runtimeDefaultDb = await DbBase.init({ namespace: TEST_NAMESPACE, token: TEST_ACCESS_TOKEN, region })
const runtimeDefaultDb = await DbBase.init({ namespace: nonProdNs, token: TEST_ACCESS_TOKEN, region })
expect(runtimeDefaultDb.serviceUrl).toBe(runtimeProdUrl)

// Simulate "aio app dev" by setting AIO_DEV
process.env.AIO_DEV = 'true'

process.env.AIO_DB_ENVIRONMENT = STAGE_ENV
const runtimeStageDevDb = await DbBase.init({ namespace: TEST_NAMESPACE, token: TEST_ACCESS_TOKEN, region })
const runtimeStageDevDb = await DbBase.init({ namespace: nonProdNs, token: TEST_ACCESS_TOKEN, region })
expect(runtimeStageDevDb.serviceUrl).toBe(stageUrl)

process.env.AIO_DB_ENVIRONMENT = PROD_ENV
const runtimeProdDevDb = await DbBase.init({ namespace: TEST_NAMESPACE, token: TEST_ACCESS_TOKEN, region })
const runtimeProdDevDb = await DbBase.init({ namespace: nonProdNs, token: TEST_ACCESS_TOKEN, region })
expect(runtimeProdDevDb.serviceUrl).toBe(externalProdUrl)

delete process.env.AIO_DB_ENVIRONMENT
const runtimeDefaultDevDb = await DbBase.init({ namespace: TEST_NAMESPACE, token: TEST_ACCESS_TOKEN, region })
const runtimeDefaultDevDb = await DbBase.init({ namespace: nonProdNs, token: TEST_ACCESS_TOKEN, region })
expect(runtimeDefaultDevDb.serviceUrl).toBe(externalProdUrl)

// Test without __OW_ACTIVATION_ID (non-runtime context)
Expand All @@ -233,30 +237,30 @@ describe('DbBase tests', () => {

process.env.AIO_DB_ENVIRONMENT = STAGE_ENV
// Stage endpoint is the same for both contexts
const externalStageDb = await DbBase.init({ namespace: TEST_NAMESPACE, token: TEST_ACCESS_TOKEN, region })
const externalStageDb = await DbBase.init({ namespace: nonProdNs, token: TEST_ACCESS_TOKEN, region })
expect(externalStageDb.serviceUrl).toBe(stageUrl)

process.env.AIO_DB_ENVIRONMENT = PROD_ENV
const externalProdDb = await DbBase.init({ namespace: TEST_NAMESPACE, token: TEST_ACCESS_TOKEN, region })
const externalProdDb = await DbBase.init({ namespace: nonProdNs, token: TEST_ACCESS_TOKEN, region })
expect(externalProdDb.serviceUrl).toBe(externalProdUrl)

delete process.env.AIO_DB_ENVIRONMENT
const externalDefaultDb = await DbBase.init({ namespace: TEST_NAMESPACE, token: TEST_ACCESS_TOKEN, region })
const externalDefaultDb = await DbBase.init({ namespace: nonProdNs, token: TEST_ACCESS_TOKEN, region })
expect(externalDefaultDb.serviceUrl).toBe(externalProdUrl)

// Make sure external behavior doesn't change if AIO_DEV is set
process.env.AIO_DEV = 'true'

process.env.AIO_DB_ENVIRONMENT = STAGE_ENV
const externalStageDevDb = await DbBase.init({ namespace: TEST_NAMESPACE, token: TEST_ACCESS_TOKEN, region })
const externalStageDevDb = await DbBase.init({ namespace: nonProdNs, token: TEST_ACCESS_TOKEN, region })
expect(externalStageDevDb.serviceUrl).toBe(stageUrl)

process.env.AIO_DB_ENVIRONMENT = PROD_ENV
const externalProdDevDb = await DbBase.init({ namespace: TEST_NAMESPACE, token: TEST_ACCESS_TOKEN, region })
const externalProdDevDb = await DbBase.init({ namespace: nonProdNs, token: TEST_ACCESS_TOKEN, region })
expect(externalProdDevDb.serviceUrl).toBe(externalProdUrl)

delete process.env.AIO_DB_ENVIRONMENT
const externalDefaultDevDb = await DbBase.init({ namespace: TEST_NAMESPACE, token: TEST_ACCESS_TOKEN, region })
const externalDefaultDevDb = await DbBase.init({ namespace: nonProdNs, token: TEST_ACCESS_TOKEN, region })
expect(externalDefaultDevDb.serviceUrl).toBe(externalProdUrl)
})

Expand Down Expand Up @@ -293,4 +297,32 @@ describe('DbBase tests', () => {

expect(db.region).toBe(ALLOWED_REGIONS[getCliEnv()].at(0))
})

test('db initialization should throw error when trying to use production workspace with aio app dev', async () => {
// TEST_NAMESPACE is a prod workspace in the prod runtime, nonProdRuntime is a prod workspace in the stage runtime
// Both should fail when AIO_DEV is set (aio app dev)
const nonProdRuntime = `development-${TEST_NAMESPACE}`

// Non-prod workspaces should be allowed regardless
const nonProdWorkspace = `${TEST_NAMESPACE}-nonProd`
const nonProdWorkspaceRuntimeEnv = `development-${TEST_NAMESPACE}-nonProd`

// Test that init only passes without a namespace suffix when running in dev mode
process.env.AIO_DEV = 'true'
await expect(DbBase.init({ namespace: TEST_NAMESPACE, token: TEST_ACCESS_TOKEN })).rejects.toThrow('prod')
await expect(DbBase.init({ namespace: nonProdRuntime, token: TEST_ACCESS_TOKEN })).rejects.toThrow('prod')
await expect(DbBase.init({ namespace: nonProdWorkspace, token: TEST_ACCESS_TOKEN })).resolves.not.toThrow()
await expect(
DbBase.init({ namespace: nonProdWorkspaceRuntimeEnv, token: TEST_ACCESS_TOKEN })
).resolves.not.toThrow()

// Test that init accepts any namespace when not running in dev mode
delete process.env.AIO_DEV
await expect(DbBase.init({ namespace: TEST_NAMESPACE, token: TEST_ACCESS_TOKEN })).resolves.not.toThrow()
await expect(DbBase.init({ namespace: nonProdWorkspace, token: TEST_ACCESS_TOKEN })).resolves.not.toThrow()
await expect(DbBase.init({ namespace: nonProdRuntime, token: TEST_ACCESS_TOKEN })).resolves.not.toThrow()
await expect(
DbBase.init({ namespace: nonProdWorkspaceRuntimeEnv, token: TEST_ACCESS_TOKEN })
).resolves.not.toThrow()
})
})
3 changes: 2 additions & 1 deletion tests/testingUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jest.mock('@adobe/aio-lib-env', () => ({
}))
const { getCliEnv } = require('@adobe/aio-lib-env')

const TEST_NAMESPACE = `testNamespace`
const TEST_NAMESPACE = `123456-testNamespace`
const TEST_ACCESS_TOKEN = 'iamatesttoken'

const TEST_REQ_CONFIG = {
Expand All @@ -37,6 +37,7 @@ beforeEach(() => {
getCliEnv.mockReturnValue(PROD_ENV)
delete process.env.__OW_ACTIVATION_ID // Ensure running in the default context
delete process.env.AIO_DB_ENDPOINT // Ensure no endpoint override
delete process.env.AIO_DEV // Ensure not running in dev mode
})

function getDb() {
Expand Down
28 changes: 28 additions & 0 deletions tests/utils/runtimeNamespace.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
Copyright 2025 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

const { isProdWorkspace } = require('../../utils/runtimeNamespace')

describe('Runtime Namespace Utils Tests', () => {
test.each([
{ namespace: '123456-test1Project', expected: 'passes' },
{ namespace: 'development-123456-test2Project', expected: 'passes' },
{ namespace: '123456-test3Project-stage', expected: 'fails' },
{ namespace: 'development-123456-test4Project-stage', expected: 'fails' },
{ namespace: 'dev-123456-test5Project', expected: 'fails' },
{ namespace: 'test6Project', expected: 'fails' },
{ namespace: '123456a-test7Project', expected: 'fails' },
{ namespace: '123456-test@Project', expected: 'fails' }
])('isProdWorkspace() $expected when runtime namespace is $namespace', ({ namespace, expected }) => {
expect(isProdWorkspace(namespace)).toBe(expected === 'passes')
})
})
27 changes: 27 additions & 0 deletions utils/runtimeNamespace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
Copyright 2025 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

const prodWorkspaceCheck = new RegExp(`^(development-)?\\d+-[a-z0-9]+$`, 'i')

/**
* Checks if the namespace is for a production workspace
* Production: 123456-testProject / development-123456-testProject
* Non-production: 123456-testProject-<tag> / development-123456-testProject-<tag>
*
* @param {string} runtimeNamespace
* @return {boolean}
**/
function isProdWorkspace(runtimeNamespace) {
return prodWorkspaceCheck.test(runtimeNamespace)
}

module.exports = { isProdWorkspace }