mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-30 04:05:40 +00:00
UBERF-8425: Account DB unit tests (#7994)
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
parent
3a5fbf6d6e
commit
1a76ebcc80
@ -13,10 +13,447 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
import { type WorkspaceUuid } from '@hcengineering/core'
|
||||
import { MongoDbCollection, WorkspaceStatusMongoDbCollection } from '../collections/mongo'
|
||||
import { Collection, Db } from 'mongodb'
|
||||
import { type WorkspaceMode, type WorkspaceUuid, type PersonUuid, SocialIdType, AccountRole } from '@hcengineering/core'
|
||||
import {
|
||||
MongoDbCollection,
|
||||
AccountMongoDbCollection,
|
||||
SocialIdMongoDbCollection,
|
||||
WorkspaceStatusMongoDbCollection,
|
||||
MongoAccountDB
|
||||
} from '../collections/mongo'
|
||||
import { WorkspaceInfoWithStatus, WorkspaceStatus } from '../types'
|
||||
|
||||
interface TestWorkspace {
|
||||
_id?: string
|
||||
uuid: WorkspaceUuid
|
||||
mode: WorkspaceMode
|
||||
name: string
|
||||
processingAttempts?: number
|
||||
lastProcessingTime?: number
|
||||
}
|
||||
|
||||
describe('MongoDbCollection', () => {
|
||||
let mockCollection: Partial<Collection<TestWorkspace>>
|
||||
let mockDb: Partial<Db>
|
||||
let collection: MongoDbCollection<TestWorkspace, 'uuid'>
|
||||
|
||||
beforeEach(() => {
|
||||
mockCollection = {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
insertOne: jest.fn(),
|
||||
updateOne: jest.fn(),
|
||||
deleteMany: jest.fn(),
|
||||
createIndex: jest.fn(),
|
||||
dropIndex: jest.fn(),
|
||||
listIndexes: jest.fn()
|
||||
}
|
||||
|
||||
mockDb = {
|
||||
collection: jest.fn().mockReturnValue(mockCollection)
|
||||
}
|
||||
|
||||
collection = new MongoDbCollection<TestWorkspace, 'uuid'>('workspace', mockDb as Db, 'uuid')
|
||||
})
|
||||
|
||||
describe('find', () => {
|
||||
it('should find documents and remove _id field', async () => {
|
||||
const mockDocs = [
|
||||
{ _id: 'id1', uuid: 'ws1' as WorkspaceUuid, mode: 'active' as const, name: 'Workspace 1' },
|
||||
{ _id: 'id2', uuid: 'ws2' as WorkspaceUuid, mode: 'active' as const, name: 'Workspace 2' }
|
||||
]
|
||||
|
||||
// Define type for our mock cursor
|
||||
interface MockCursor {
|
||||
sort: jest.Mock
|
||||
limit: jest.Mock
|
||||
map: jest.Mock
|
||||
toArray: jest.Mock
|
||||
transform?: (doc: any) => any
|
||||
}
|
||||
|
||||
// Create a mock cursor that properly implements the map functionality
|
||||
const mockCursor: MockCursor = {
|
||||
sort: jest.fn().mockReturnThis(),
|
||||
limit: jest.fn().mockReturnThis(),
|
||||
map: jest.fn().mockImplementation(function (this: MockCursor, callback) {
|
||||
this.transform = callback
|
||||
return this
|
||||
}),
|
||||
toArray: jest.fn().mockImplementation(function (this: MockCursor) {
|
||||
return Promise.resolve(this.transform != null ? mockDocs.map(this.transform) : mockDocs)
|
||||
})
|
||||
}
|
||||
|
||||
;(mockCollection.find as jest.Mock).mockReturnValue(mockCursor)
|
||||
|
||||
const result = await collection.find({ mode: 'active' as const })
|
||||
|
||||
expect(mockCollection.find).toHaveBeenCalledWith({ mode: 'active' })
|
||||
expect(result).toEqual([
|
||||
{ uuid: 'ws1' as WorkspaceUuid, mode: 'active' as const, name: 'Workspace 1' },
|
||||
{ uuid: 'ws2' as WorkspaceUuid, mode: 'active' as const, name: 'Workspace 2' }
|
||||
])
|
||||
})
|
||||
|
||||
it('should apply sort and limit', async () => {
|
||||
const mockFind = {
|
||||
sort: jest.fn().mockReturnThis(),
|
||||
limit: jest.fn().mockReturnThis(),
|
||||
toArray: jest.fn().mockResolvedValue([]),
|
||||
map: jest.fn().mockReturnThis()
|
||||
}
|
||||
;(mockCollection.find as jest.Mock).mockReturnValue(mockFind)
|
||||
|
||||
await collection.find({ mode: 'active' as const }, { name: 'ascending', processingAttempts: 'descending' }, 10)
|
||||
|
||||
expect(mockFind.sort).toHaveBeenCalledWith({
|
||||
name: 'ascending',
|
||||
processingAttempts: 'descending'
|
||||
})
|
||||
expect(mockFind.limit).toHaveBeenCalledWith(10)
|
||||
})
|
||||
})
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should find single document and remove _id field', async () => {
|
||||
const mockDoc = {
|
||||
_id: 'id1',
|
||||
uuid: 'ws1' as WorkspaceUuid,
|
||||
mode: 'active' as const,
|
||||
name: 'Workspace 1'
|
||||
}
|
||||
const expectedDoc = { ...mockDoc }
|
||||
delete (expectedDoc as any)._id
|
||||
;(mockCollection.findOne as jest.Mock).mockResolvedValue(mockDoc)
|
||||
|
||||
const result = await collection.findOne({ uuid: 'ws1' as WorkspaceUuid })
|
||||
|
||||
expect(mockCollection.findOne).toHaveBeenCalledWith({ uuid: 'ws1' })
|
||||
expect(result).toEqual(expectedDoc)
|
||||
})
|
||||
})
|
||||
|
||||
describe('insertOne', () => {
|
||||
it('should insert document with generated UUID', async () => {
|
||||
const doc = {
|
||||
mode: 'pending-creation' as const,
|
||||
name: 'New Workspace'
|
||||
}
|
||||
|
||||
await collection.insertOne(doc)
|
||||
|
||||
expect(mockCollection.insertOne).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
mode: 'pending-creation',
|
||||
name: 'New Workspace'
|
||||
})
|
||||
)
|
||||
|
||||
// Get the actual document passed to insertOne
|
||||
const insertedDoc = (mockCollection.insertOne as jest.Mock).mock.calls[0][0]
|
||||
|
||||
// Check that uuid was generated
|
||||
expect(insertedDoc.uuid).toBeDefined()
|
||||
// Check that _id matches uuid
|
||||
expect(insertedDoc._id).toBe(insertedDoc.uuid)
|
||||
})
|
||||
|
||||
it('should use provided UUID if available', async () => {
|
||||
const doc = {
|
||||
uuid: 'custom-uuid' as WorkspaceUuid,
|
||||
mode: 'pending-creation' as const,
|
||||
name: 'New Workspace'
|
||||
}
|
||||
|
||||
await collection.insertOne(doc)
|
||||
|
||||
expect(mockCollection.insertOne).toHaveBeenCalledWith({
|
||||
...doc,
|
||||
_id: 'custom-uuid'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateOne', () => {
|
||||
it('should handle simple field updates', async () => {
|
||||
await collection.updateOne({ uuid: 'ws1' as WorkspaceUuid }, { mode: 'creating' as const })
|
||||
|
||||
expect(mockCollection.updateOne).toHaveBeenCalledWith({ uuid: 'ws1' }, { $set: { mode: 'creating' } })
|
||||
})
|
||||
|
||||
it('should handle increment operations', async () => {
|
||||
await collection.updateOne(
|
||||
{ uuid: 'ws1' as WorkspaceUuid },
|
||||
{
|
||||
$inc: { processingAttempts: 1 },
|
||||
mode: 'upgrading' as const
|
||||
}
|
||||
)
|
||||
|
||||
expect(mockCollection.updateOne).toHaveBeenCalledWith(
|
||||
{ uuid: 'ws1' },
|
||||
{
|
||||
$inc: { processingAttempts: 1 },
|
||||
$set: { mode: 'upgrading' }
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteMany', () => {
|
||||
it('should delete documents matching query', async () => {
|
||||
await collection.deleteMany({ mode: 'deleted' as const })
|
||||
|
||||
expect(mockCollection.deleteMany).toHaveBeenCalledWith({ mode: 'deleted' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('ensureIndices', () => {
|
||||
it('should create new indices', async () => {
|
||||
;(mockCollection.listIndexes as jest.Mock).mockReturnValue({
|
||||
toArray: jest.fn().mockResolvedValue([{ key: { _id: 1 }, name: '_id_' }])
|
||||
})
|
||||
|
||||
const indices = [
|
||||
{
|
||||
key: { uuid: 1 },
|
||||
options: { unique: true, name: 'uuid_1' }
|
||||
},
|
||||
{
|
||||
key: { mode: 1 },
|
||||
options: { name: 'mode_1' }
|
||||
}
|
||||
]
|
||||
|
||||
await collection.ensureIndices(indices)
|
||||
|
||||
expect(mockCollection.createIndex).toHaveBeenCalledTimes(2)
|
||||
expect(mockCollection.createIndex).toHaveBeenCalledWith({ uuid: 1 }, { unique: true, name: 'uuid_1' })
|
||||
expect(mockCollection.createIndex).toHaveBeenCalledWith({ mode: 1 }, { name: 'mode_1' })
|
||||
})
|
||||
|
||||
it('should drop unused indices', async () => {
|
||||
;(mockCollection.listIndexes as jest.Mock).mockReturnValue({
|
||||
toArray: jest.fn().mockResolvedValue([
|
||||
{ key: { _id: 1 }, name: '_id_' },
|
||||
{ key: { oldField: 1 }, name: 'oldField_1' }
|
||||
])
|
||||
})
|
||||
|
||||
const indices = [
|
||||
{
|
||||
key: { uuid: 1 },
|
||||
options: { unique: true, name: 'uuid_1' }
|
||||
}
|
||||
]
|
||||
|
||||
await collection.ensureIndices(indices)
|
||||
|
||||
expect(mockCollection.dropIndex).toHaveBeenCalledWith('oldField_1')
|
||||
expect(mockCollection.createIndex).toHaveBeenCalledWith({ uuid: 1 }, { unique: true, name: 'uuid_1' })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('AccountMongoDbCollection', () => {
|
||||
let mockCollection: any
|
||||
let mockDb: any
|
||||
let collection: AccountMongoDbCollection
|
||||
|
||||
beforeEach(() => {
|
||||
mockCollection = {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
insertOne: jest.fn(),
|
||||
updateOne: jest.fn()
|
||||
}
|
||||
|
||||
mockDb = {
|
||||
collection: jest.fn().mockReturnValue(mockCollection)
|
||||
}
|
||||
|
||||
collection = new AccountMongoDbCollection(mockDb)
|
||||
})
|
||||
|
||||
describe('find', () => {
|
||||
// Define type for our mock cursor
|
||||
interface MockCursor {
|
||||
sort: jest.Mock
|
||||
limit: jest.Mock
|
||||
map: jest.Mock
|
||||
toArray: jest.Mock
|
||||
transform?: (doc: any) => any
|
||||
}
|
||||
|
||||
it('should convert Buffer fields in found documents', async () => {
|
||||
const mockDocs = [
|
||||
{
|
||||
_id: 'id1',
|
||||
uuid: 'acc1' as PersonUuid,
|
||||
hash: { buffer: new Uint8Array([1, 2, 3]) },
|
||||
salt: { buffer: new Uint8Array([4, 5, 6]) }
|
||||
}
|
||||
]
|
||||
|
||||
const mockCursor: MockCursor = {
|
||||
sort: jest.fn().mockReturnThis(),
|
||||
limit: jest.fn().mockReturnThis(),
|
||||
map: jest.fn().mockImplementation(function (this: MockCursor, callback) {
|
||||
this.transform = callback
|
||||
return this
|
||||
}),
|
||||
toArray: jest.fn().mockImplementation(function (this: MockCursor) {
|
||||
return Promise.resolve(this.transform != null ? mockDocs.map(this.transform) : mockDocs)
|
||||
})
|
||||
}
|
||||
|
||||
mockCollection.find.mockReturnValue(mockCursor)
|
||||
|
||||
const results = await collection.find({ uuid: 'acc1' as PersonUuid })
|
||||
|
||||
expect(results[0].hash).toBeInstanceOf(Buffer)
|
||||
expect(results[0].salt).toBeInstanceOf(Buffer)
|
||||
expect(Buffer.from(results[0].hash as any).toString('hex')).toBe(Buffer.from([1, 2, 3]).toString('hex'))
|
||||
expect(Buffer.from(results[0].salt as any).toString('hex')).toBe(Buffer.from([4, 5, 6]).toString('hex'))
|
||||
})
|
||||
|
||||
it('should handle null hash and salt in found documents', async () => {
|
||||
const mockDocs = [
|
||||
{
|
||||
_id: 'id1',
|
||||
uuid: 'acc1' as PersonUuid,
|
||||
hash: null,
|
||||
salt: null
|
||||
}
|
||||
]
|
||||
|
||||
const mockCursor: MockCursor = {
|
||||
sort: jest.fn().mockReturnThis(),
|
||||
limit: jest.fn().mockReturnThis(),
|
||||
map: jest.fn().mockImplementation(function (this: MockCursor, callback) {
|
||||
this.transform = callback
|
||||
return this
|
||||
}),
|
||||
toArray: jest.fn().mockImplementation(function (this: MockCursor) {
|
||||
return Promise.resolve(this.transform != null ? mockDocs.map(this.transform) : mockDocs)
|
||||
})
|
||||
}
|
||||
|
||||
mockCollection.find.mockReturnValue(mockCursor)
|
||||
|
||||
const results = await collection.find({ uuid: 'acc1' as PersonUuid })
|
||||
|
||||
expect(results[0].hash).toBeNull()
|
||||
expect(results[0].salt).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should convert Buffer fields in found document', async () => {
|
||||
const mockDoc = {
|
||||
_id: 'id1',
|
||||
uuid: 'acc1' as PersonUuid,
|
||||
hash: { buffer: new Uint8Array([1, 2, 3]) },
|
||||
salt: { buffer: new Uint8Array([4, 5, 6]) }
|
||||
}
|
||||
|
||||
mockCollection.findOne.mockResolvedValue(mockDoc)
|
||||
|
||||
const result = await collection.findOne({ uuid: 'acc1' as PersonUuid })
|
||||
|
||||
expect(result?.hash).toBeInstanceOf(Buffer)
|
||||
expect(result?.salt).toBeInstanceOf(Buffer)
|
||||
expect(Buffer.from(result?.hash as any).toString('hex')).toBe(Buffer.from([1, 2, 3]).toString('hex'))
|
||||
expect(Buffer.from(result?.salt as any).toString('hex')).toBe(Buffer.from([4, 5, 6]).toString('hex'))
|
||||
})
|
||||
|
||||
it('should handle null hash and salt in found document', async () => {
|
||||
const mockDoc = {
|
||||
_id: 'id1',
|
||||
uuid: 'acc1' as PersonUuid,
|
||||
hash: null,
|
||||
salt: null
|
||||
}
|
||||
|
||||
mockCollection.findOne.mockResolvedValue(mockDoc)
|
||||
|
||||
const result = await collection.findOne({ uuid: 'acc1' as PersonUuid })
|
||||
|
||||
expect(result?.hash).toBeNull()
|
||||
expect(result?.salt).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle null result', async () => {
|
||||
mockCollection.findOne.mockResolvedValue(null)
|
||||
|
||||
const result = await collection.findOne({ uuid: 'non-existent' as PersonUuid })
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('SocialIdMongoDbCollection', () => {
|
||||
let mockCollection: any
|
||||
let mockDb: any
|
||||
let collection: SocialIdMongoDbCollection
|
||||
|
||||
beforeEach(() => {
|
||||
mockCollection = {
|
||||
insertOne: jest.fn()
|
||||
}
|
||||
|
||||
mockDb = {
|
||||
collection: jest.fn().mockReturnValue(mockCollection)
|
||||
}
|
||||
|
||||
collection = new SocialIdMongoDbCollection(mockDb)
|
||||
})
|
||||
|
||||
describe('insertOne', () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('should throw error if type is missing', async () => {
|
||||
const socialId = {
|
||||
value: 'test@example.com',
|
||||
personUuid: 'person1' as PersonUuid
|
||||
}
|
||||
|
||||
await expect(collection.insertOne(socialId)).rejects.toThrow('Type and value are required')
|
||||
})
|
||||
|
||||
it('should throw error if value is missing', async () => {
|
||||
const socialId = {
|
||||
type: SocialIdType.EMAIL,
|
||||
personUuid: 'person1' as PersonUuid
|
||||
}
|
||||
|
||||
await expect(collection.insertOne(socialId)).rejects.toThrow('Type and value are required')
|
||||
})
|
||||
|
||||
it('should generate key', async () => {
|
||||
const socialId = {
|
||||
type: SocialIdType.EMAIL,
|
||||
value: 'test@example.com',
|
||||
personUuid: 'person1' as PersonUuid
|
||||
}
|
||||
|
||||
await collection.insertOne(socialId)
|
||||
|
||||
expect(mockCollection.insertOne).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
...socialId,
|
||||
key: 'email:test@example.com'
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('WorkspaceStatusMongoDbCollection', () => {
|
||||
let mockWsCollection: MongoDbCollection<WorkspaceInfoWithStatus, 'uuid'>
|
||||
let wsStatusCollection: WorkspaceStatusMongoDbCollection
|
||||
@ -224,3 +661,248 @@ describe('WorkspaceStatusMongoDbCollection', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('MongoAccountDB', () => {
|
||||
let mockDb: any
|
||||
let accountDb: MongoAccountDB
|
||||
let mockAccount: any
|
||||
let mockWorkspace: any
|
||||
let mockWorkspaceMembers: any
|
||||
let mockWorkspaceStatus: any
|
||||
|
||||
beforeEach(() => {
|
||||
mockDb = {}
|
||||
|
||||
// Create mock collections with jest.fn()
|
||||
mockAccount = {
|
||||
updateOne: jest.fn(),
|
||||
ensureIndices: jest.fn()
|
||||
}
|
||||
|
||||
mockWorkspace = {
|
||||
updateOne: jest.fn(),
|
||||
insertOne: jest.fn(),
|
||||
find: jest.fn(),
|
||||
ensureIndices: jest.fn()
|
||||
}
|
||||
|
||||
mockWorkspaceMembers = {
|
||||
insertOne: jest.fn(),
|
||||
deleteMany: jest.fn(),
|
||||
updateOne: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
find: jest.fn(),
|
||||
ensureIndices: jest.fn()
|
||||
}
|
||||
|
||||
mockWorkspaceStatus = {
|
||||
insertOne: jest.fn()
|
||||
}
|
||||
|
||||
accountDb = new MongoAccountDB(mockDb)
|
||||
|
||||
// Override the getters to return our mocks
|
||||
Object.defineProperties(accountDb, {
|
||||
account: { get: () => mockAccount },
|
||||
workspace: { get: () => mockWorkspace },
|
||||
workspaceMembers: { get: () => mockWorkspaceMembers },
|
||||
workspaceStatus: { get: () => mockWorkspaceStatus }
|
||||
})
|
||||
})
|
||||
|
||||
describe('init', () => {
|
||||
it('should create required indices', async () => {
|
||||
await accountDb.init()
|
||||
|
||||
// Verify account indices
|
||||
expect(accountDb.account.ensureIndices).toHaveBeenCalledWith([
|
||||
{
|
||||
key: { uuid: 1 },
|
||||
options: { unique: true, name: 'hc_account_account_uuid_1' }
|
||||
}
|
||||
])
|
||||
|
||||
// Verify workspace indices
|
||||
expect(accountDb.workspace.ensureIndices).toHaveBeenCalledWith([
|
||||
{
|
||||
key: { uuid: 1 },
|
||||
options: {
|
||||
unique: true,
|
||||
name: 'hc_account_workspace_uuid_1'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: { url: 1 },
|
||||
options: {
|
||||
unique: true,
|
||||
name: 'hc_account_workspace_url_1'
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// Verify workspace members indices
|
||||
expect(accountDb.workspaceMembers.ensureIndices).toHaveBeenCalledWith([
|
||||
{
|
||||
key: { workspaceUuid: 1 },
|
||||
options: {
|
||||
name: 'hc_account_workspace_members_workspace_uuid_1'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: { accountUuid: 1 },
|
||||
options: {
|
||||
name: 'hc_account_workspace_members_account_uuid_1'
|
||||
}
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('workspace operations', () => {
|
||||
const accountId = 'acc1' as PersonUuid
|
||||
const workspaceId = 'ws1' as WorkspaceUuid
|
||||
const role = AccountRole.Owner
|
||||
|
||||
describe('assignWorkspace', () => {
|
||||
it('should insert workspace member', async () => {
|
||||
await accountDb.assignWorkspace(accountId, workspaceId, role)
|
||||
|
||||
expect(accountDb.workspaceMembers.insertOne).toHaveBeenCalledWith({
|
||||
workspaceUuid: workspaceId,
|
||||
accountUuid: accountId,
|
||||
role
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('unassignWorkspace', () => {
|
||||
it('should delete workspace member', async () => {
|
||||
await accountDb.unassignWorkspace(accountId, workspaceId)
|
||||
|
||||
expect(accountDb.workspaceMembers.deleteMany).toHaveBeenCalledWith({
|
||||
workspaceUuid: workspaceId,
|
||||
accountUuid: accountId
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateWorkspaceRole', () => {
|
||||
it('should update member role', async () => {
|
||||
await accountDb.updateWorkspaceRole(accountId, workspaceId, role)
|
||||
|
||||
expect(accountDb.workspaceMembers.updateOne).toHaveBeenCalledWith(
|
||||
{
|
||||
workspaceUuid: workspaceId,
|
||||
accountUuid: accountId
|
||||
},
|
||||
{ role }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getWorkspaceRole', () => {
|
||||
it('should return role when member exists', async () => {
|
||||
;(accountDb.workspaceMembers.findOne as jest.Mock).mockResolvedValue({ role })
|
||||
|
||||
const result = await accountDb.getWorkspaceRole(accountId, workspaceId)
|
||||
|
||||
expect(result).toBe(role)
|
||||
})
|
||||
|
||||
it('should return null when member does not exist', async () => {
|
||||
;(accountDb.workspaceMembers.findOne as jest.Mock).mockResolvedValue(null)
|
||||
|
||||
const result = await accountDb.getWorkspaceRole(accountId, workspaceId)
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getWorkspaceMembers', () => {
|
||||
it('should return mapped member info', async () => {
|
||||
const members = [
|
||||
{ accountUuid: 'acc1' as PersonUuid, role: AccountRole.Owner },
|
||||
{ accountUuid: 'acc2' as PersonUuid, role: AccountRole.Maintainer }
|
||||
]
|
||||
|
||||
;(accountDb.workspaceMembers.find as jest.Mock).mockResolvedValue(members)
|
||||
|
||||
const result = await accountDb.getWorkspaceMembers(workspaceId)
|
||||
|
||||
expect(result).toEqual([
|
||||
{ person: 'acc1', role: AccountRole.Owner },
|
||||
{ person: 'acc2', role: AccountRole.Maintainer }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAccountWorkspaces', () => {
|
||||
it('should return workspaces for account', async () => {
|
||||
const members = [{ workspaceUuid: 'ws1' as WorkspaceUuid }, { workspaceUuid: 'ws2' as WorkspaceUuid }]
|
||||
const workspaces = [
|
||||
{ uuid: 'ws1', name: 'Workspace 1' },
|
||||
{ uuid: 'ws2', name: 'Workspace 2' }
|
||||
]
|
||||
|
||||
;(accountDb.workspaceMembers.find as jest.Mock).mockResolvedValue(members)
|
||||
;(accountDb.workspace.find as jest.Mock).mockResolvedValue(workspaces)
|
||||
|
||||
const result = await accountDb.getAccountWorkspaces(accountId)
|
||||
|
||||
expect(result).toEqual(workspaces)
|
||||
expect(accountDb.workspace.find).toHaveBeenCalledWith({
|
||||
uuid: { $in: ['ws1', 'ws2'] }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('createWorkspace', () => {
|
||||
it('should create workspace and status', async () => {
|
||||
const workspaceData = {
|
||||
name: 'New Workspace',
|
||||
url: 'new-workspace'
|
||||
}
|
||||
const statusData = {
|
||||
mode: 'active' as const,
|
||||
versionMajor: 1,
|
||||
versionMinor: 0,
|
||||
versionPatch: 0,
|
||||
isDisabled: false
|
||||
}
|
||||
|
||||
;(accountDb.workspace.insertOne as jest.Mock).mockResolvedValue('ws1')
|
||||
|
||||
const result = await accountDb.createWorkspace(workspaceData, statusData)
|
||||
|
||||
expect(result).toBe('ws1')
|
||||
expect(accountDb.workspace.insertOne).toHaveBeenCalledWith(workspaceData)
|
||||
expect(accountDb.workspaceStatus.insertOne).toHaveBeenCalledWith({
|
||||
workspaceUuid: 'ws1',
|
||||
...statusData
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('password operations', () => {
|
||||
const accountId = 'acc1' as PersonUuid
|
||||
const passwordHash = Buffer.from('hash')
|
||||
const salt = Buffer.from('salt')
|
||||
|
||||
describe('setPassword', () => {
|
||||
it('should update account with password hash and salt', async () => {
|
||||
await accountDb.setPassword(accountId, passwordHash, salt)
|
||||
|
||||
expect(accountDb.account.updateOne).toHaveBeenCalledWith({ uuid: accountId }, { hash: passwordHash, salt })
|
||||
})
|
||||
})
|
||||
|
||||
describe('resetPassword', () => {
|
||||
it('should reset password hash and salt to null', async () => {
|
||||
await accountDb.resetPassword(accountId)
|
||||
|
||||
expect(accountDb.account.updateOne).toHaveBeenCalledWith({ uuid: accountId }, { hash: null, salt: null })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
615
server/account/src/__tests__/postgres.test.ts
Normal file
615
server/account/src/__tests__/postgres.test.ts
Normal file
@ -0,0 +1,615 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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 CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import {
|
||||
AccountRole,
|
||||
Data,
|
||||
Version,
|
||||
type PersonUuid,
|
||||
type WorkspaceMode,
|
||||
type WorkspaceUuid
|
||||
} from '@hcengineering/core'
|
||||
import { AccountPostgresDbCollection, PostgresAccountDB, PostgresDbCollection } from '../collections/postgres'
|
||||
import { Sql } from 'postgres'
|
||||
|
||||
interface TestWorkspace {
|
||||
uuid: WorkspaceUuid
|
||||
mode: WorkspaceMode
|
||||
name: string
|
||||
processingAttempts?: number
|
||||
lastProcessingTime?: number
|
||||
}
|
||||
|
||||
describe('PostgresDbCollection', () => {
|
||||
let mockClient: any
|
||||
let collection: PostgresDbCollection<TestWorkspace, 'uuid'>
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = {
|
||||
unsafe: jest.fn().mockResolvedValue([]) // Default to empty array result
|
||||
}
|
||||
|
||||
collection = new PostgresDbCollection<TestWorkspace, 'uuid'>('workspace', mockClient as Sql, 'uuid')
|
||||
})
|
||||
|
||||
describe('getTableName', () => {
|
||||
it('should return table name with default namespace', () => {
|
||||
expect(collection.getTableName()).toBe('global_account.workspace')
|
||||
})
|
||||
|
||||
it('should return table name without namespace when ns is empty', () => {
|
||||
collection = new PostgresDbCollection<TestWorkspace, 'uuid'>('workspace', mockClient as Sql, 'uuid', '')
|
||||
expect(collection.getTableName()).toBe('workspace')
|
||||
})
|
||||
|
||||
it('should return table name with custom namespace when ns is provided', () => {
|
||||
collection = new PostgresDbCollection<TestWorkspace, 'uuid'>(
|
||||
'workspace',
|
||||
mockClient as Sql,
|
||||
'uuid',
|
||||
'custom_account'
|
||||
)
|
||||
expect(collection.getTableName()).toBe('custom_account.workspace')
|
||||
})
|
||||
})
|
||||
|
||||
describe('find', () => {
|
||||
it('should generate simple query', async () => {
|
||||
await collection.find({ mode: 'active' as const })
|
||||
|
||||
expect(mockClient.unsafe).toHaveBeenCalledWith('SELECT * FROM global_account.workspace WHERE "mode" = $1', [
|
||||
'active'
|
||||
])
|
||||
})
|
||||
|
||||
it('should handle $in operator', async () => {
|
||||
await collection.find({
|
||||
mode: { $in: ['active' as const, 'creating' as const] }
|
||||
})
|
||||
|
||||
expect(mockClient.unsafe).toHaveBeenCalledWith(
|
||||
'SELECT * FROM global_account.workspace WHERE "mode" IN ($1, $2)',
|
||||
['active', 'creating']
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle comparison operators', async () => {
|
||||
await collection.find({
|
||||
processingAttempts: { $lt: 3 },
|
||||
lastProcessingTime: { $gt: 1000 }
|
||||
})
|
||||
|
||||
expect(mockClient.unsafe).toHaveBeenCalledWith(
|
||||
'SELECT * FROM global_account.workspace WHERE "processing_attempts" < $1 AND "last_processing_time" > $2',
|
||||
[3, 1000]
|
||||
)
|
||||
})
|
||||
|
||||
it('should apply sort', async () => {
|
||||
await collection.find({ mode: 'active' as const }, { lastProcessingTime: 'descending', name: 'ascending' })
|
||||
|
||||
expect(mockClient.unsafe).toHaveBeenCalledWith(
|
||||
'SELECT * FROM global_account.workspace WHERE "mode" = $1 ORDER BY "last_processing_time" DESC, "name" ASC',
|
||||
['active']
|
||||
)
|
||||
})
|
||||
|
||||
it('should apply limit', async () => {
|
||||
await collection.find({ mode: 'active' as const }, undefined, 10)
|
||||
|
||||
expect(mockClient.unsafe).toHaveBeenCalledWith(
|
||||
'SELECT * FROM global_account.workspace WHERE "mode" = $1 LIMIT 10',
|
||||
['active']
|
||||
)
|
||||
})
|
||||
|
||||
it('should convert snake_case to camelCase in results', async () => {
|
||||
mockClient.unsafe.mockResolvedValue([
|
||||
{
|
||||
uuid: 'ws1',
|
||||
mode: 'active',
|
||||
name: 'Test',
|
||||
processing_attempts: 1,
|
||||
last_processing_time: 1000
|
||||
}
|
||||
])
|
||||
|
||||
const result = await collection.find({})
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
uuid: 'ws1',
|
||||
mode: 'active',
|
||||
name: 'Test',
|
||||
processingAttempts: 1,
|
||||
lastProcessingTime: 1000
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should use find with limit 1', async () => {
|
||||
await collection.findOne({ uuid: 'ws1' as WorkspaceUuid })
|
||||
|
||||
expect(mockClient.unsafe).toHaveBeenCalledWith(
|
||||
'SELECT * FROM global_account.workspace WHERE "uuid" = $1 LIMIT 1',
|
||||
['ws1']
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('insertOne', () => {
|
||||
it('should generate insert query with returning', async () => {
|
||||
mockClient.unsafe.mockResolvedValue([{ uuid: 'ws1' }])
|
||||
|
||||
const doc = {
|
||||
mode: 'pending-creation' as const,
|
||||
name: 'New Workspace'
|
||||
}
|
||||
|
||||
await collection.insertOne(doc)
|
||||
|
||||
expect(mockClient.unsafe).toHaveBeenCalledWith(
|
||||
'INSERT INTO global_account.workspace ("mode", "name") VALUES ($1, $2) RETURNING *',
|
||||
['pending-creation', 'New Workspace']
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateOne', () => {
|
||||
it('should handle simple field updates', async () => {
|
||||
await collection.updateOne({ uuid: 'ws1' as WorkspaceUuid }, { mode: 'creating' as const })
|
||||
|
||||
expect(mockClient.unsafe).toHaveBeenCalledWith(
|
||||
'UPDATE global_account.workspace SET "mode" = $1 WHERE "uuid" = $2',
|
||||
['creating', 'ws1']
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle increment operations', async () => {
|
||||
await collection.updateOne({ uuid: 'ws1' as WorkspaceUuid }, { $inc: { processingAttempts: 1 } })
|
||||
|
||||
expect(mockClient.unsafe).toHaveBeenCalledWith(
|
||||
'UPDATE global_account.workspace SET "processing_attempts" = "processing_attempts" + $1 WHERE "uuid" = $2',
|
||||
[1, 'ws1']
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteMany', () => {
|
||||
it('should generate delete query', async () => {
|
||||
await collection.deleteMany({ mode: 'deleted' as const })
|
||||
|
||||
expect(mockClient.unsafe).toHaveBeenCalledWith('DELETE FROM global_account.workspace WHERE "mode" = $1', [
|
||||
'deleted'
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('AccountPostgresDbCollection', () => {
|
||||
let mockClient: any
|
||||
let collection: AccountPostgresDbCollection
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = {
|
||||
unsafe: jest.fn().mockResolvedValue([])
|
||||
}
|
||||
|
||||
collection = new AccountPostgresDbCollection(mockClient as Sql)
|
||||
})
|
||||
|
||||
describe('getTableName', () => {
|
||||
it('should return correct table name', () => {
|
||||
expect(collection.getTableName()).toBe('global_account.account')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPasswordsTableName', () => {
|
||||
it('should return correct passwords table name', () => {
|
||||
expect(collection.getPasswordsTableName()).toBe('global_account.account_passwords')
|
||||
})
|
||||
})
|
||||
|
||||
describe('find', () => {
|
||||
it('should join with passwords table', async () => {
|
||||
const mockResult = [
|
||||
{
|
||||
uuid: 'acc1' as PersonUuid,
|
||||
timezone: 'UTC',
|
||||
locale: 'en',
|
||||
hash: null,
|
||||
salt: null
|
||||
}
|
||||
]
|
||||
mockClient.unsafe.mockResolvedValue(mockResult)
|
||||
|
||||
const result = await collection.find({ uuid: 'acc1' as PersonUuid })
|
||||
|
||||
expect(mockClient.unsafe).toHaveBeenCalledWith(
|
||||
`SELECT * FROM (
|
||||
SELECT
|
||||
a.uuid,
|
||||
a.timezone,
|
||||
a.locale,
|
||||
p.hash,
|
||||
p.salt
|
||||
FROM global_account.account as a
|
||||
LEFT JOIN global_account.account_passwords as p ON p.account_uuid = a.uuid
|
||||
) WHERE "uuid" = $1`,
|
||||
['acc1']
|
||||
)
|
||||
expect(result).toEqual(mockResult)
|
||||
})
|
||||
|
||||
it('should convert buffer fields from database', async () => {
|
||||
const mockResult = [
|
||||
{
|
||||
uuid: 'acc1' as PersonUuid,
|
||||
timezone: 'UTC',
|
||||
locale: 'en',
|
||||
hash: { 0: 1, 1: 2, 3: 3 }, // Simulating buffer data from DB
|
||||
salt: { 0: 4, 1: 5, 2: 6 }
|
||||
}
|
||||
]
|
||||
mockClient.unsafe.mockResolvedValue(mockResult)
|
||||
|
||||
const result = await collection.find({ uuid: 'acc1' as PersonUuid })
|
||||
|
||||
expect(result[0].hash).toBeInstanceOf(Buffer)
|
||||
expect(result[0].salt).toBeInstanceOf(Buffer)
|
||||
expect(Buffer.from(result[0].hash as any).toString('hex')).toBe(Buffer.from([1, 2, 3]).toString('hex'))
|
||||
expect(Buffer.from(result[0].salt as any).toString('hex')).toBe(Buffer.from([4, 5, 6]).toString('hex'))
|
||||
})
|
||||
|
||||
it('should throw error if querying by password fields', async () => {
|
||||
await expect(collection.find({ hash: Buffer.from([]) })).rejects.toThrow(
|
||||
'Passwords are not allowed in find query conditions'
|
||||
)
|
||||
await expect(collection.find({ salt: Buffer.from([]) })).rejects.toThrow(
|
||||
'Passwords are not allowed in find query conditions'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('insertOne', () => {
|
||||
it('should prevent inserting password fields', async () => {
|
||||
const doc = {
|
||||
uuid: 'acc1' as PersonUuid,
|
||||
hash: Buffer.from([]),
|
||||
salt: Buffer.from([])
|
||||
}
|
||||
|
||||
await expect(collection.insertOne(doc)).rejects.toThrow('Passwords are not allowed in insert query')
|
||||
})
|
||||
|
||||
it('should allow inserting non-password fields', async () => {
|
||||
const doc = {
|
||||
uuid: 'acc1' as PersonUuid,
|
||||
timezone: 'UTC',
|
||||
locale: 'en'
|
||||
}
|
||||
mockClient.unsafe.mockResolvedValue([{ uuid: 'acc1' }])
|
||||
|
||||
await collection.insertOne(doc)
|
||||
|
||||
expect(mockClient.unsafe).toHaveBeenCalledWith(
|
||||
'INSERT INTO global_account.account ("uuid", "timezone", "locale") VALUES ($1, $2, $3) RETURNING *',
|
||||
['acc1', 'UTC', 'en']
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateOne', () => {
|
||||
it('should prevent updating with password fields in query', async () => {
|
||||
await expect(collection.updateOne({ hash: Buffer.from([]) }, { timezone: 'UTC' })).rejects.toThrow(
|
||||
'Passwords are not allowed in update query'
|
||||
)
|
||||
})
|
||||
|
||||
it('should prevent updating password fields', async () => {
|
||||
await expect(
|
||||
collection.updateOne({ uuid: 'acc1' as PersonUuid }, { hash: Buffer.from([]), salt: Buffer.from([]) })
|
||||
).rejects.toThrow('Passwords are not allowed in update query')
|
||||
})
|
||||
|
||||
it('should allow updating non-password fields', async () => {
|
||||
await collection.updateOne({ uuid: 'acc1' as PersonUuid }, { timezone: 'UTC', locale: 'en' })
|
||||
|
||||
expect(mockClient.unsafe).toHaveBeenCalledWith(
|
||||
'UPDATE global_account.account SET "timezone" = $1, "locale" = $2 WHERE "uuid" = $3',
|
||||
['UTC', 'en', 'acc1']
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteMany', () => {
|
||||
it('should prevent deleting by password fields', async () => {
|
||||
await expect(collection.deleteMany({ hash: Buffer.from([]) })).rejects.toThrow(
|
||||
'Passwords are not allowed in delete query'
|
||||
)
|
||||
})
|
||||
|
||||
it('should allow deleting by non-password fields', async () => {
|
||||
await collection.deleteMany({ uuid: 'acc1' as PersonUuid })
|
||||
|
||||
expect(mockClient.unsafe).toHaveBeenCalledWith('DELETE FROM global_account.account WHERE "uuid" = $1', ['acc1'])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('PostgresAccountDB', () => {
|
||||
let mockClient: any
|
||||
let accountDb: PostgresAccountDB
|
||||
let spyTag: jest.Mock
|
||||
let spyValue: any = []
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a spy that returns a Promise
|
||||
spyTag = jest.fn().mockImplementation(() => Promise.resolve(spyValue))
|
||||
|
||||
// Create base function that's also a tag function
|
||||
const mock: any = Object.assign(spyTag, {
|
||||
unsafe: jest.fn().mockResolvedValue([]),
|
||||
begin: jest.fn().mockImplementation((callback) => callback(mock))
|
||||
})
|
||||
|
||||
mockClient = mock
|
||||
accountDb = new PostgresAccountDB(mockClient)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
spyValue = []
|
||||
})
|
||||
|
||||
describe('init', () => {
|
||||
it('should apply migrations in transaction', async () => {
|
||||
spyValue = {
|
||||
count: 1
|
||||
}
|
||||
await accountDb.migrate('test_migration', 'CREATE TABLE test')
|
||||
|
||||
expect(mockClient.begin).toHaveBeenCalled()
|
||||
expect(mockClient).toHaveBeenCalledWith(
|
||||
['INSERT INTO _account_applied_migrations (identifier, ddl) VALUES (', ', ', ') ON CONFLICT DO NOTHING'],
|
||||
'test_migration',
|
||||
'CREATE TABLE test'
|
||||
)
|
||||
expect(mockClient.unsafe).toHaveBeenCalledWith('CREATE TABLE test')
|
||||
})
|
||||
})
|
||||
|
||||
describe('workspace operations', () => {
|
||||
const accountId = 'acc1' as PersonUuid
|
||||
const workspaceId = 'ws1' as WorkspaceUuid
|
||||
const role = AccountRole.Owner
|
||||
|
||||
describe('workspace member operations', () => {
|
||||
it('should assign workspace member', async () => {
|
||||
await accountDb.assignWorkspace(accountId, workspaceId, role)
|
||||
|
||||
expect(mockClient).toHaveBeenCalledWith('global_account.workspace_members')
|
||||
expect(mockClient).toHaveBeenCalledWith(
|
||||
['INSERT INTO ', ' (workspace_uuid, account_uuid, role) VALUES (', ', ', ', ', ')'],
|
||||
expect.anything(),
|
||||
workspaceId,
|
||||
accountId,
|
||||
role
|
||||
)
|
||||
})
|
||||
|
||||
it('should unassign workspace member', async () => {
|
||||
await accountDb.unassignWorkspace(accountId, workspaceId)
|
||||
|
||||
expect(mockClient).toHaveBeenCalledWith('global_account.workspace_members')
|
||||
expect(mockClient).toHaveBeenCalledWith(
|
||||
['DELETE FROM ', ' WHERE workspace_uuid = ', ' AND account_uuid = ', ''],
|
||||
expect.anything(),
|
||||
workspaceId,
|
||||
accountId
|
||||
)
|
||||
})
|
||||
|
||||
it('should update workspace role', async () => {
|
||||
await accountDb.updateWorkspaceRole(accountId, workspaceId, role)
|
||||
|
||||
expect(mockClient).toHaveBeenCalledWith('global_account.workspace_members')
|
||||
expect(mockClient).toHaveBeenCalledWith(
|
||||
['UPDATE ', ' SET role = ', ' WHERE workspace_uuid = ', ' AND account_uuid = ', ''],
|
||||
expect.anything(),
|
||||
role,
|
||||
workspaceId,
|
||||
accountId
|
||||
)
|
||||
})
|
||||
|
||||
it('should get workspace role', async () => {
|
||||
mockClient.unsafe.mockResolvedValue([{ role }])
|
||||
|
||||
await accountDb.getWorkspaceRole(accountId, workspaceId)
|
||||
|
||||
expect(mockClient).toHaveBeenCalledWith('global_account.workspace_members')
|
||||
expect(mockClient).toHaveBeenCalledWith(
|
||||
['SELECT role FROM ', ' WHERE workspace_uuid = ', ' AND account_uuid = ', ''],
|
||||
expect.anything(),
|
||||
workspaceId,
|
||||
accountId
|
||||
)
|
||||
})
|
||||
|
||||
it('should get workspace members', async () => {
|
||||
spyValue = [
|
||||
{ account_uuid: 'acc1', role: AccountRole.Owner },
|
||||
{ account_uuid: 'acc2', role: AccountRole.Maintainer }
|
||||
]
|
||||
|
||||
const result = await accountDb.getWorkspaceMembers(workspaceId)
|
||||
|
||||
expect(mockClient).toHaveBeenCalledWith('global_account.workspace_members')
|
||||
expect(mockClient).toHaveBeenCalledWith(
|
||||
['SELECT account_uuid, role FROM ', ' WHERE workspace_uuid = ', ''],
|
||||
expect.anything(),
|
||||
workspaceId
|
||||
)
|
||||
|
||||
expect(result).toEqual([
|
||||
{ person: 'acc1', role: AccountRole.Owner },
|
||||
{ person: 'acc2', role: AccountRole.Maintainer }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAccountWorkspaces', () => {
|
||||
it('should return workspaces with status and converted keys', async () => {
|
||||
const mockWorkspaces = [
|
||||
{
|
||||
uuid: workspaceId,
|
||||
name: 'Test',
|
||||
url: 'test',
|
||||
status: {
|
||||
mode: 'active',
|
||||
version_major: 1,
|
||||
version_minor: 0,
|
||||
version_patch: 0,
|
||||
is_disabled: false
|
||||
}
|
||||
}
|
||||
]
|
||||
mockClient.unsafe.mockResolvedValue(mockWorkspaces)
|
||||
|
||||
const res = await accountDb.getAccountWorkspaces(accountId)
|
||||
|
||||
expect(mockClient.unsafe).toHaveBeenCalledWith(expect.any(String), [accountId])
|
||||
expect(res[0]).toEqual({
|
||||
uuid: workspaceId,
|
||||
name: 'Test',
|
||||
url: 'test',
|
||||
status: {
|
||||
mode: 'active',
|
||||
versionMajor: 1,
|
||||
versionMinor: 0,
|
||||
versionPatch: 0,
|
||||
isDisabled: false
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPendingWorkspace', () => {
|
||||
const version: Data<Version> = { major: 1, minor: 0, patch: 0 }
|
||||
const processingTimeoutMs = 5000
|
||||
const NOW = 1234567890000 // Fixed timestamp
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Date, 'now').mockReturnValue(NOW)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('should get pending creation workspace', async () => {
|
||||
await accountDb.getPendingWorkspace('', version, 'create', processingTimeoutMs)
|
||||
|
||||
expect(mockClient.unsafe.mock.calls[0][0].replace(/\s+/g, ' ')).toEqual(
|
||||
`SELECT
|
||||
w.uuid,
|
||||
w.name,
|
||||
w.url,
|
||||
w.branding,
|
||||
w.location,
|
||||
w.region,
|
||||
w.created_by,
|
||||
w.created_on,
|
||||
w.billing_account,
|
||||
json_build_object(
|
||||
'mode', s.mode,
|
||||
'processing_progress', s.processing_progress,
|
||||
'version_major', s.version_major,
|
||||
'version_minor', s.version_minor,
|
||||
'version_patch', s.version_patch,
|
||||
'last_processing_time', s.last_processing_time,
|
||||
'last_visit', s.last_visit,
|
||||
'is_disabled', s.is_disabled,
|
||||
'processing_attempts', s.processing_attempts,
|
||||
'processing_message', s.processing_message,
|
||||
'backup_info', s.backup_info
|
||||
) status
|
||||
FROM global_account.workspace as w
|
||||
INNER JOIN global_account.workspace_status as s ON s.workspace_uuid = w.uuid
|
||||
WHERE s.mode IN ('pending-creation', 'creating')
|
||||
AND s.mode <> 'manual-creation'
|
||||
AND (s.processing_attempts IS NULL OR s.processing_attempts <= 3)
|
||||
AND (s.last_processing_time IS NULL OR s.last_processing_time < $1)
|
||||
AND (w.region IS NULL OR w.region = '')
|
||||
ORDER BY s.last_visit DESC
|
||||
LIMIT 1
|
||||
FOR UPDATE SKIP LOCKED`.replace(/\s+/g, ' ')
|
||||
)
|
||||
expect(mockClient.unsafe.mock.calls[0][1]).toEqual([NOW - processingTimeoutMs])
|
||||
})
|
||||
|
||||
// Should also verify update after fetch
|
||||
it('should update processing attempts and time after fetch', async () => {
|
||||
const wsUuid = 'ws1'
|
||||
mockClient.unsafe.mockResolvedValueOnce([{ uuid: wsUuid }]) // Mock the fetch result
|
||||
|
||||
await accountDb.getPendingWorkspace('', version, 'create', processingTimeoutMs)
|
||||
|
||||
// Verify the update was called
|
||||
expect(mockClient.unsafe.mock.calls[1][0].replace(/\s+/g, ' ')).toEqual(
|
||||
`UPDATE global_account.workspace_status
|
||||
SET processing_attempts = processing_attempts + 1, "last_processing_time" = $1
|
||||
WHERE workspace_uuid = $2`.replace(/\s+/g, ' ')
|
||||
)
|
||||
expect(mockClient.unsafe.mock.calls[1][1]).toEqual([NOW, wsUuid])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('password operations', () => {
|
||||
const accountId = 'acc1' as PersonUuid
|
||||
const hash: any = {
|
||||
buffer: Buffer.from('hash')
|
||||
}
|
||||
const salt: any = {
|
||||
buffer: Buffer.from('salt')
|
||||
}
|
||||
|
||||
it('should set password', async () => {
|
||||
await accountDb.setPassword(accountId, hash, salt)
|
||||
|
||||
expect(mockClient).toHaveBeenCalledWith('global_account.account_passwords')
|
||||
expect(mockClient).toHaveBeenCalledWith(
|
||||
['UPSERT INTO ', ' (account_uuid, hash, salt) VALUES (', ', ', '::bytea, ', '::bytea)'],
|
||||
expect.anything(),
|
||||
accountId,
|
||||
hash.buffer,
|
||||
salt.buffer
|
||||
)
|
||||
})
|
||||
|
||||
it('should reset password', async () => {
|
||||
await accountDb.resetPassword(accountId)
|
||||
|
||||
expect(mockClient).toHaveBeenCalledWith('global_account.account_passwords')
|
||||
expect(mockClient).toHaveBeenCalledWith(
|
||||
['DELETE FROM ', ' WHERE account_uuid = ', ''],
|
||||
expect.anything(),
|
||||
accountId
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
@ -141,7 +141,13 @@ implements DbCollection<T> {
|
||||
}
|
||||
|
||||
async findOne (query: Query<T>): Promise<T | null> {
|
||||
return await this.collection.findOne<T>(query as Filter<T>)
|
||||
const doc = await this.collection.findOne<T>(query as Filter<T>)
|
||||
if (doc === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
delete doc._id
|
||||
return doc
|
||||
}
|
||||
|
||||
async insertOne (data: Partial<T>): Promise<K extends keyof T ? T[K] : undefined> {
|
||||
@ -189,7 +195,7 @@ export class AccountMongoDbCollection extends MongoDbCollection<Account, 'uuid'>
|
||||
super('account', db, 'uuid')
|
||||
}
|
||||
|
||||
convertToObj (acc: Account): Account {
|
||||
private convertToObj (acc: Account): Account {
|
||||
return {
|
||||
...acc,
|
||||
hash: acc.hash != null ? Buffer.from(acc.hash.buffer) : acc.hash,
|
||||
|
Loading…
Reference in New Issue
Block a user