tests: vaults (#2365)

* tests: signature tests

* tests: asymmetric

* feat: delete contact use case

* chore: lint

* chore: lint
This commit is contained in:
Mo 2023-07-24 14:28:28 -05:00 committed by GitHub
parent d6bcc808d5
commit 98c0228139
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 649 additions and 145 deletions

View File

@ -1,3 +1,4 @@
import { SessionsClientInterface } from './../Session/SessionsClientInterface'
import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface'
import { GetUntrustedPayload } from './UseCase/GetUntrustedPayload'
import { GetInboundMessages } from './UseCase/GetInboundMessages'
@ -31,6 +32,7 @@ describe('AsymmetricMessageService', () => {
let sync: jest.Mocked<SyncServiceInterface>
let mutator: jest.Mocked<MutatorClientInterface>
let encryption: jest.Mocked<EncryptionProviderInterface>
let sessions: jest.Mocked<SessionsClientInterface>
let service: AsymmetricMessageService
beforeEach(() => {
@ -60,9 +62,10 @@ describe('AsymmetricMessageService', () => {
eventBus.addEventHandler = jest.fn()
service = new AsymmetricMessageService(
messageServer,
encryption,
mutator,
sessions,
messageServer,
createOrEditContact,
findContact,
getAllContacts,

View File

@ -1,3 +1,4 @@
import { SessionsClientInterface } from './../Session/SessionsClientInterface'
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
import { AsymmetricMessageServerHash, ClientDisplayableError, isClientDisplayableError } from '@standardnotes/responses'
import { SyncEvent, SyncEventReceivedAsymmetricMessagesData } from '../Event/SyncEvent'
@ -39,9 +40,10 @@ export class AsymmetricMessageService
implements AsymmetricMessageServiceInterface, InternalEventHandlerInterface
{
constructor(
private messageServer: AsymmetricMessageServer,
private encryption: EncryptionProviderInterface,
private mutator: MutatorClientInterface,
private sessions: SessionsClientInterface,
private messageServer: AsymmetricMessageServer,
private _createOrEditContact: CreateOrEditContact,
private _findContact: FindContact,
private _getAllContacts: GetAllContacts,
@ -184,10 +186,6 @@ export class AsymmetricMessageService
message: AsymmetricMessageServerHash,
payload: AsymmetricMessagePayload,
): Promise<void> {
if (payload.data.recipientUuid !== message.user_uuid) {
return
}
if (payload.type === AsymmetricMessagePayloadType.ContactShare) {
await this.handleTrustedContactShareMessage(message, payload)
} else if (payload.type === AsymmetricMessagePayloadType.SenderKeypairChanged) {
@ -225,6 +223,7 @@ export class AsymmetricMessageService
const result = this._getTrustedPayload.execute({
privateKey: this.encryption.getKeyPair().privateKey,
sender: contact.getValue(),
ownUserUuid: this.sessions.userUuid,
message,
})

View File

@ -0,0 +1,63 @@
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
import { AsymmetricMessagePayload, TrustedContactInterface } from '@standardnotes/models'
import { DecryptMessage } from '../../Encryption/UseCase/Asymmetric/DecryptMessage'
import { Result } from '@standardnotes/domain-core'
import { GetTrustedPayload } from './GetTrustedPayload'
describe('GetTrustedPayload', () => {
let decryptMessage: jest.Mocked<DecryptMessage>
let getTrustedPayload: GetTrustedPayload
beforeEach(() => {
decryptMessage = {} as jest.Mocked<DecryptMessage>
decryptMessage.execute = jest.fn()
getTrustedPayload = new GetTrustedPayload(decryptMessage)
})
describe('execute', () => {
const mockDto = {
privateKey: 'test-private-key',
message: {} as AsymmetricMessageServerHash,
sender: {} as TrustedContactInterface,
ownUserUuid: 'test-user-uuid',
}
it('should return failure when decryption fails', () => {
decryptMessage.execute = jest.fn().mockReturnValue(Result.fail('Decryption failed'))
const result = getTrustedPayload.execute(mockDto)
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Decryption failed')
})
it('should return failure when recipientUuid is not equal to ownUserUuid', () => {
const mockPayload: AsymmetricMessagePayload = {
data: {
recipientUuid: 'another-user-uuid',
},
} as AsymmetricMessagePayload
decryptMessage.execute = jest.fn().mockReturnValue(Result.ok(mockPayload))
const result = getTrustedPayload.execute(mockDto)
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Message is not for this user')
})
it('should return success when recipientUuid is equal to ownUserUuid', () => {
const mockPayload: AsymmetricMessagePayload = {
data: {
recipientUuid: 'test-user-uuid',
},
} as AsymmetricMessagePayload
decryptMessage.execute = jest.fn().mockReturnValue(Result.ok(mockPayload))
const result = getTrustedPayload.execute(mockDto)
expect(result.isFailed()).toBe(false)
expect(result.getValue()).toBe(mockPayload)
})
})
})

View File

@ -10,6 +10,7 @@ export class GetTrustedPayload implements SyncUseCaseInterface<AsymmetricMessage
privateKey: string
message: AsymmetricMessageServerHash
sender: TrustedContactInterface
ownUserUuid: string
}): Result<M> {
const result = this.decryptMessage.execute<M>({
message: dto.message.encrypted_message,
@ -17,6 +18,16 @@ export class GetTrustedPayload implements SyncUseCaseInterface<AsymmetricMessage
privateKey: dto.privateKey,
})
if (result.isFailed()) {
return result
}
const recipientUuid = result.getValue().data.recipientUuid
if (recipientUuid !== dto.ownUserUuid) {
return Result.fail('Message is not for this user')
}
return result
}
}

View File

@ -1,3 +1,4 @@
import { DeleteContact } from './UseCase/DeleteContact'
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
import { UserKeyPairChangedEventData } from './../Session/UserKeyPairChangedEventData'
import { SessionEvent } from './../Session/SessionEvent'
@ -21,6 +22,7 @@ import { CreateOrEditContact } from './UseCase/CreateOrEditContact'
import { EditContact } from './UseCase/EditContact'
import { GetAllContacts } from './UseCase/GetAllContacts'
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
import { Result } from '@standardnotes/domain-core'
export class ContactService
extends AbstractService<ContactServiceEvent>
@ -34,6 +36,7 @@ export class ContactService
private user: UserClientInterface,
private selfContactManager: SelfContactManager,
private encryption: EncryptionProviderInterface,
private _deleteContact: DeleteContact,
private _findContact: FindContact,
private _getAllContacts: GetAllContacts,
private _createOrEditContact: CreateOrEditContact,
@ -167,20 +170,15 @@ export class ContactService
return contact
}
async deleteContact(contact: TrustedContactInterface): Promise<void> {
if (contact.isMe) {
throw new Error('Cannot delete self')
}
await this.mutator.setItemToBeDeleted(contact)
await this.sync.sync()
async deleteContact(contact: TrustedContactInterface): Promise<Result<void>> {
return this._deleteContact.execute({ contact, ownUserUuid: this.session.userUuid })
}
getAllContacts(): TrustedContactInterface[] {
return this._getAllContacts.execute().getValue()
}
findTrustedContact(userUuid: string): TrustedContactInterface | undefined {
findContact(userUuid: string): TrustedContactInterface | undefined {
const result = this._findContact.execute({ userUuid })
if (result.isFailed()) {
return undefined
@ -188,12 +186,12 @@ export class ContactService
return result.getValue()
}
findTrustedContactForServerUser(user: SharedVaultUserServerHash): TrustedContactInterface | undefined {
return this.findTrustedContact(user.user_uuid)
findContactForServerUser(user: SharedVaultUserServerHash): TrustedContactInterface | undefined {
return this.findContact(user.user_uuid)
}
findTrustedContactForInvite(invite: SharedVaultInviteServerHash): TrustedContactInterface | undefined {
return this.findTrustedContact(invite.sender_uuid)
findContactForInvite(invite: SharedVaultInviteServerHash): TrustedContactInterface | undefined {
return this.findContact(invite.sender_uuid)
}
getCollaborationIDForTrustedContact(contact: TrustedContactInterface): string {
@ -205,7 +203,7 @@ export class ContactService
})
}
isItemAuthenticallySigned(item: DecryptedItemInterface): ItemSignatureValidationResult {
getItemSignatureStatus(item: DecryptedItemInterface): ItemSignatureValidationResult {
return this._validateItemSigner.execute(item)
}

View File

@ -2,6 +2,7 @@ import { DecryptedItemInterface, TrustedContactInterface } from '@standardnotes/
import { AbstractService } from '../Service/AbstractService'
import { SharedVaultInviteServerHash, SharedVaultUserServerHash } from '@standardnotes/responses'
import { ItemSignatureValidationResult } from './UseCase/Types/ItemSignatureValidationResult'
import { Result } from '@standardnotes/domain-core'
export enum ContactServiceEvent {}
@ -26,13 +27,13 @@ export interface ContactServiceInterface extends AbstractService<ContactServiceE
contact: TrustedContactInterface,
params: { name: string; collaborationID: string },
): Promise<TrustedContactInterface>
deleteContact(contact: TrustedContactInterface): Promise<void>
deleteContact(contact: TrustedContactInterface): Promise<Result<void>>
getAllContacts(): TrustedContactInterface[]
getSelfContact(): TrustedContactInterface | undefined
findTrustedContact(userUuid: string): TrustedContactInterface | undefined
findTrustedContactForServerUser(user: SharedVaultUserServerHash): TrustedContactInterface | undefined
findTrustedContactForInvite(invite: SharedVaultInviteServerHash): TrustedContactInterface | undefined
findContact(userUuid: string): TrustedContactInterface | undefined
findContactForServerUser(user: SharedVaultUserServerHash): TrustedContactInterface | undefined
findContactForInvite(invite: SharedVaultInviteServerHash): TrustedContactInterface | undefined
isItemAuthenticallySigned(item: DecryptedItemInterface): ItemSignatureValidationResult
getItemSignatureStatus(item: DecryptedItemInterface): ItemSignatureValidationResult
}

View File

@ -0,0 +1,87 @@
import { ContactBelongsToVault } from './../../SharedVaults/UseCase/ContactBelongsToVault'
import { GetOwnedSharedVaults } from './../../SharedVaults/UseCase/GetOwnedSharedVaults'
import { SyncServiceInterface } from './../../Sync/SyncServiceInterface'
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface'
import { Result } from '@standardnotes/domain-core'
import { TrustedContactInterface } from '@standardnotes/models'
import { DeleteContact } from './DeleteContact'
describe('DeleteContact', () => {
let mutator: jest.Mocked<MutatorClientInterface>
let sync: jest.Mocked<SyncServiceInterface>
let getOwnedVaults: jest.Mocked<GetOwnedSharedVaults>
let contactBelongsToVault: jest.Mocked<ContactBelongsToVault>
let deleteContact: DeleteContact
let mockDto: {
contact: TrustedContactInterface
ownUserUuid: string
}
let mockVaults: { id: string }[]
beforeEach(() => {
mutator = {} as jest.Mocked<MutatorClientInterface>
sync = {} as jest.Mocked<SyncServiceInterface>
getOwnedVaults = {} as jest.Mocked<GetOwnedSharedVaults>
getOwnedVaults.execute = jest.fn()
contactBelongsToVault = {} as jest.Mocked<ContactBelongsToVault>
contactBelongsToVault.execute = jest.fn()
deleteContact = new DeleteContact(mutator, sync, getOwnedVaults, contactBelongsToVault)
mockDto = {
contact: { isMe: false } as TrustedContactInterface,
ownUserUuid: 'test-user-uuid',
}
mockVaults = [{ id: 'vault1' }, { id: 'vault2' }]
})
describe('execute', () => {
it('should throw error when deleting self', async () => {
mockDto.contact.isMe = true
await expect(deleteContact.execute(mockDto)).rejects.toThrow('Cannot delete self')
})
it('should return failure when getting owned vaults fails', async () => {
getOwnedVaults.execute = jest.fn().mockReturnValue(Result.fail('Failed to get owned vaults'))
const result = await deleteContact.execute(mockDto)
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Failed to get owned vaults')
})
it('should return failure when checking contact membership fails', async () => {
getOwnedVaults.execute = jest.fn().mockReturnValue(Result.ok(mockVaults))
contactBelongsToVault.execute = jest.fn().mockResolvedValue(Result.fail('Failed to check contact membership'))
const result = await deleteContact.execute(mockDto)
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Failed to check contact membership')
})
it('should return failure when contact belongs to an owned vault', async () => {
getOwnedVaults.execute = jest.fn().mockReturnValue(Result.ok(mockVaults))
contactBelongsToVault.execute = jest.fn().mockResolvedValue(Result.ok(true))
const result = await deleteContact.execute(mockDto)
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Cannot delete contact that belongs to an owned vault')
})
it('should return success when contact is successfully deleted', async () => {
getOwnedVaults.execute = jest.fn().mockReturnValue(Result.ok(mockVaults))
contactBelongsToVault.execute = jest.fn().mockResolvedValue(Result.ok(false))
mutator.setItemToBeDeleted = jest.fn().mockResolvedValue(undefined)
sync.sync = jest.fn().mockResolvedValue(undefined)
const result = await deleteContact.execute(mockDto)
expect(result.isFailed()).toBe(false)
})
})
})

View File

@ -0,0 +1,45 @@
import { ContactBelongsToVault } from './../../SharedVaults/UseCase/ContactBelongsToVault'
import { GetOwnedSharedVaults } from './../../SharedVaults/UseCase/GetOwnedSharedVaults'
import { SyncServiceInterface } from './../../Sync/SyncServiceInterface'
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { TrustedContactInterface } from '@standardnotes/models'
export class DeleteContact implements UseCaseInterface<void> {
constructor(
private mutator: MutatorClientInterface,
private sync: SyncServiceInterface,
private getOwnedVaults: GetOwnedSharedVaults,
private contactBelongsToVault: ContactBelongsToVault,
) {}
async execute(dto: { contact: TrustedContactInterface; ownUserUuid: string }): Promise<Result<void>> {
if (dto.contact.isMe) {
throw new Error('Cannot delete self')
}
const vaults = this.getOwnedVaults.execute({ userUuid: dto.ownUserUuid })
if (vaults.isFailed()) {
return Result.fail('Failed to get owned vaults')
}
for (const vault of vaults.getValue()) {
const contactBelongsToVault = await this.contactBelongsToVault.execute({
contact: dto.contact,
vault: vault,
})
if (contactBelongsToVault.isFailed()) {
return Result.fail('Failed to check contact membership')
}
if (contactBelongsToVault.getValue()) {
return Result.fail('Cannot delete contact that belongs to an owned vault')
}
}
await this.mutator.setItemToBeDeleted(dto.contact)
await this.sync.sync()
return Result.ok()
}
}

View File

@ -1,4 +1,6 @@
import { IsVaultAdmin } from './../VaultUser/UseCase/IsVaultAdmin'
import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface'
import { GetOwnedSharedVaults } from './UseCase/GetOwnedSharedVaults'
import { IsVaultOwner } from './../VaultUser/UseCase/IsVaultOwner'
import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface'
import { DeleteSharedVault } from './UseCase/DeleteSharedVault'
import { ConvertToSharedVault } from './UseCase/ConvertToSharedVault'
@ -14,8 +16,6 @@ import { SharedVaultService } from './SharedVaultService'
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { SessionsClientInterface } from '../Session/SessionsClientInterface'
import { VaultServiceInterface } from '../Vaults/VaultServiceInterface'
import { InternalEventBusInterface } from '../..'
import { ContactPublicKeySetInterface, TrustedContactInterface } from '@standardnotes/models'
describe('SharedVaultService', () => {
@ -30,8 +30,8 @@ describe('SharedVaultService', () => {
const encryption = {} as jest.Mocked<EncryptionProviderInterface>
const session = {} as jest.Mocked<SessionsClientInterface>
const vaults = {} as jest.Mocked<VaultServiceInterface>
const getVault = {} as jest.Mocked<GetVault>
const getOwnedVaults = {} as jest.Mocked<GetOwnedSharedVaults>
const createSharedVaultUseCase = {} as jest.Mocked<CreateSharedVault>
const handleKeyPairChange = {} as jest.Mocked<HandleKeyPairChange>
const notifyVaultUsersOfKeyRotation = {} as jest.Mocked<NotifyVaultUsersOfKeyRotation>
@ -41,7 +41,7 @@ describe('SharedVaultService', () => {
const shareContactWithVault = {} as jest.Mocked<ShareContactWithVault>
const convertToSharedVault = {} as jest.Mocked<ConvertToSharedVault>
const deleteSharedVaultUseCase = {} as jest.Mocked<DeleteSharedVault>
const isVaultAdmin = {} as jest.Mocked<IsVaultAdmin>
const isVaultAdmin = {} as jest.Mocked<IsVaultOwner>
const eventBus = {} as jest.Mocked<InternalEventBusInterface>
eventBus.addEventHandler = jest.fn()
@ -50,8 +50,8 @@ describe('SharedVaultService', () => {
items,
encryption,
session,
vaults,
getVault,
getOwnedVaults,
createSharedVaultUseCase,
handleKeyPairChange,
notifyVaultUsersOfKeyRotation,

View File

@ -18,7 +18,6 @@ import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface
import { SyncEvent } from '../Event/SyncEvent'
import { SessionEvent } from '../Session/SessionEvent'
import { InternalEventInterface } from '../Internal/InternalEventInterface'
import { VaultServiceInterface } from '../Vaults/VaultServiceInterface'
import { UserEventServiceEvent, UserEventServiceEventPayload } from '../UserEvent/UserEventServiceEvent'
import { DeleteThirdPartyVault } from './UseCase/DeleteExternalSharedVault'
import { DeleteSharedVault } from './UseCase/DeleteSharedVault'
@ -33,7 +32,8 @@ import { ContentType } from '@standardnotes/domain-core'
import { HandleKeyPairChange } from '../Contacts/UseCase/HandleKeyPairChange'
import { FindContact } from '../Contacts/UseCase/FindContact'
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
import { IsVaultAdmin } from '../VaultUser/UseCase/IsVaultAdmin'
import { IsVaultOwner } from '../VaultUser/UseCase/IsVaultOwner'
import { GetOwnedSharedVaults } from './UseCase/GetOwnedSharedVaults'
export class SharedVaultService
extends AbstractService<SharedVaultServiceEvent, SharedVaultServiceEventPayload>
@ -43,9 +43,9 @@ export class SharedVaultService
private items: ItemManagerInterface,
private encryption: EncryptionProviderInterface,
private session: SessionsClientInterface,
private vaults: VaultServiceInterface,
private _getVault: GetVault,
private _createSharedVaultUseCase: CreateSharedVault,
private _getOwnedSharedVaults: GetOwnedSharedVaults,
private _createSharedVault: CreateSharedVault,
private _handleKeyPairChange: HandleKeyPairChange,
private _notifyVaultUsersOfKeyRotation: NotifyVaultUsersOfKeyRotation,
private _sendVaultDataChangeMessage: SendVaultDataChangedMessage,
@ -53,8 +53,8 @@ export class SharedVaultService
private _deleteThirdPartyVault: DeleteThirdPartyVault,
private _shareContactWithVault: ShareContactWithVault,
private _convertToSharedVault: ConvertToSharedVault,
private _deleteSharedVaultUseCase: DeleteSharedVault,
private _isVaultAdmin: IsVaultAdmin,
private _deleteSharedVault: DeleteSharedVault,
private _isVaultAdmin: IsVaultOwner,
eventBus: InternalEventBusInterface,
) {
super(eventBus)
@ -81,9 +81,8 @@ export class SharedVaultService
;(this.items as unknown) = undefined
;(this.encryption as unknown) = undefined
;(this.session as unknown) = undefined
;(this.vaults as unknown) = undefined
;(this._getVault as unknown) = undefined
;(this._createSharedVaultUseCase as unknown) = undefined
;(this._createSharedVault as unknown) = undefined
;(this._handleKeyPairChange as unknown) = undefined
;(this._notifyVaultUsersOfKeyRotation as unknown) = undefined
;(this._sendVaultDataChangeMessage as unknown) = undefined
@ -91,7 +90,7 @@ export class SharedVaultService
;(this._deleteThirdPartyVault as unknown) = undefined
;(this._shareContactWithVault as unknown) = undefined
;(this._convertToSharedVault as unknown) = undefined
;(this._deleteSharedVaultUseCase as unknown) = undefined
;(this._deleteSharedVault as unknown) = undefined
;(this._isVaultAdmin as unknown) = undefined
}
@ -173,7 +172,7 @@ export class SharedVaultService
userInputtedPassword: string | undefined
storagePreference?: KeySystemRootKeyStorageMode
}): Promise<VaultListingInterface | ClientDisplayableError> {
return this._createSharedVaultUseCase.execute({
return this._createSharedVault.execute({
vaultName: dto.name,
vaultDescription: dto.description,
userInputtedPassword: dto.userInputtedPassword,
@ -187,11 +186,6 @@ export class SharedVaultService
return this._convertToSharedVault.execute({ vault })
}
private getAllSharedVaults(): SharedVaultListingInterface[] {
const vaults = this.vaults.getVaults().filter((vault) => vault.isSharedVaultListing())
return vaults as SharedVaultListingInterface[]
}
private async handleTrustedContactsChange(contacts: TrustedContactInterface[]): Promise<void> {
for (const contact of contacts) {
if (contact.isMe) {
@ -220,7 +214,7 @@ export class SharedVaultService
}
public async deleteSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void> {
return this._deleteSharedVaultUseCase.execute({ sharedVault })
return this._deleteSharedVault.execute({ sharedVault })
}
async shareContactWithVaults(contact: TrustedContactInterface): Promise<void> {
@ -228,14 +222,7 @@ export class SharedVaultService
throw new Error('Cannot share self contact')
}
const ownedVaults = this.getAllSharedVaults().filter((vault) => {
return this._isVaultAdmin
.execute({
sharedVault: vault,
userUuid: this.session.userUuid,
})
.getValue()
})
const ownedVaults = this._getOwnedSharedVaults.execute({ userUuid: this.session.userUuid }).getValue()
for (const vault of ownedVaults) {
await this._shareContactWithVault.execute({

View File

@ -0,0 +1,23 @@
import { GetVaultUsers } from './../../VaultUser/UseCase/GetVaultUsers'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { SharedVaultListingInterface, TrustedContactInterface } from '@standardnotes/models'
export class ContactBelongsToVault implements UseCaseInterface<boolean> {
constructor(private getVaultUsers: GetVaultUsers) {}
async execute(dto: {
contact: TrustedContactInterface
vault: SharedVaultListingInterface
}): Promise<Result<boolean>> {
const users = await this.getVaultUsers.execute({
sharedVaultUuid: dto.vault.sharing.sharedVaultUuid,
readFromCache: false,
})
if (users.isFailed()) {
return Result.fail('Failed to get vault users')
}
return Result.ok(users.getValue().some((u) => u.user_uuid === dto.contact.contactUuid))
}
}

View File

@ -0,0 +1,23 @@
import { IsVaultOwner } from './../../VaultUser/UseCase/IsVaultOwner'
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
import { SharedVaultListingInterface } from '@standardnotes/models'
import { GetSharedVaults } from './GetSharedVaults'
export class GetOwnedSharedVaults implements SyncUseCaseInterface<SharedVaultListingInterface[]> {
constructor(private getSharedVaults: GetSharedVaults, private isVaultOwnwer: IsVaultOwner) {}
execute(dto: { userUuid: string }): Result<SharedVaultListingInterface[]> {
const sharedVaults = this.getSharedVaults.execute().getValue()
const ownedVaults = sharedVaults.filter((vault) => {
return this.isVaultOwnwer
.execute({
sharedVault: vault,
userUuid: dto.userUuid,
})
.getValue()
})
return Result.ok(ownedVaults)
}
}

View File

@ -0,0 +1,16 @@
import { GetVaults } from './../../Vaults/UseCase/GetVaults'
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
import { SharedVaultListingInterface } from '@standardnotes/models'
export class GetSharedVaults implements SyncUseCaseInterface<SharedVaultListingInterface[]> {
constructor(private getVaults: GetVaults) {}
execute(): Result<SharedVaultListingInterface[]> {
const vaults = this.getVaults
.execute()
.getValue()
.filter((vault) => vault.isSharedVaultListing())
return Result.ok(vaults as SharedVaultListingInterface[])
}
}

View File

@ -44,7 +44,10 @@ export class NotifyVaultUsersOfKeyRotation implements UseCaseInterface<void> {
await this.deleteAllInvites(params.sharedVault.sharing.sharedVaultUuid)
const contacts = await this.getVaultContacts.execute(params.sharedVault.sharing.sharedVaultUuid)
const contacts = await this.getVaultContacts.execute({
sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid,
readFromCache: false,
})
for (const invite of existingInvites.getValue()) {
const recipient = this.findContact.execute({ userUuid: invite.user_uuid })

View File

@ -29,13 +29,16 @@ export class SendVaultDataChangedMessage implements UseCaseInterface<void> {
signing: PkcKeyPair
}
}): Promise<Result<void>> {
const users = await this.getVaultUsers.execute({ sharedVaultUuid: params.vault.sharing.sharedVaultUuid })
if (!users) {
const users = await this.getVaultUsers.execute({
sharedVaultUuid: params.vault.sharing.sharedVaultUuid,
readFromCache: false,
})
if (users.isFailed()) {
return Result.fail('Cannot send metadata changed message; users not found')
}
const errors: string[] = []
for (const user of users) {
for (const user of users.getValue()) {
if (user.user_uuid === params.senderUuid) {
continue
}

View File

@ -32,14 +32,14 @@ export class SendVaultKeyChangedMessage implements UseCaseInterface<void> {
signing: PkcKeyPair
}
}): Promise<Result<void>> {
const users = await this.getVaultUsers.execute({ sharedVaultUuid: params.sharedVaultUuid })
if (!users) {
const users = await this.getVaultUsers.execute({ sharedVaultUuid: params.sharedVaultUuid, readFromCache: false })
if (users.isFailed()) {
return Result.fail('Cannot send root key changed message; users not found')
}
const errors: string[] = []
for (const user of users) {
for (const user of users.getValue()) {
if (user.user_uuid === params.senderUuid) {
continue
}

View File

@ -33,17 +33,18 @@ export class ShareContactWithVault implements UseCaseInterface<void> {
const users = await this.getVaultUsers.execute({
sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid,
readFromCache: false,
})
if (!users) {
if (users.isFailed()) {
return Result.fail('Cannot share contact; shared vault users not found')
}
if (users.length === 0) {
if (users.getValue().length === 0) {
return Result.ok()
}
for (const vaultUser of users) {
for (const vaultUser of users.getValue()) {
if (vaultUser.user_uuid === params.senderUserUuid) {
continue
}

View File

@ -175,7 +175,10 @@ export class VaultInviteService
contact: TrustedContactInterface,
permissions: SharedVaultPermission,
): Promise<Result<SharedVaultInviteServerHash>> {
const contactsResult = await this._getVaultContacts.execute(sharedVault.sharing.sharedVaultUuid)
const contactsResult = await this._getVaultContacts.execute({
sharedVaultUuid: sharedVault.sharing.sharedVaultUuid,
readFromCache: false,
})
if (contactsResult.isFailed()) {
return Result.fail(contactsResult.getError())
}
@ -241,6 +244,7 @@ export class VaultInviteService
const trustedMessage = this._getTrustedPayload.execute<AsymmetricMessageSharedVaultInvite>({
message: invite,
privateKey: this.encryption.getKeyPair().privateKey,
ownUserUuid: this.session.userUuid,
sender: sender.getValue(),
})

View File

@ -7,13 +7,17 @@ import { Result, UseCaseInterface } from '@standardnotes/domain-core'
export class GetVaultContacts implements UseCaseInterface<TrustedContactInterface[]> {
constructor(private findContact: FindContact, private getVaultUsers: GetVaultUsers) {}
async execute(sharedVaultUuid: string): Promise<Result<TrustedContactInterface[]>> {
const users = await this.getVaultUsers.execute({ sharedVaultUuid })
if (!users) {
async execute(dto: { sharedVaultUuid: string; readFromCache: boolean }): Promise<Result<TrustedContactInterface[]>> {
const users = await this.getVaultUsers.execute({
sharedVaultUuid: dto.sharedVaultUuid,
readFromCache: dto.readFromCache,
})
if (users.isFailed()) {
return Result.fail('Failed to get vault users')
}
const contacts = users
.getValue()
.map((user) => this.findContact.execute({ userUuid: user.user_uuid }))
.map((result) => (result.isFailed() ? undefined : result.getValue()))
.filter(isNotUndefined)

View File

@ -1,16 +1,31 @@
import { SharedVaultUserServerHash, isErrorResponse } from '@standardnotes/responses'
import { VaultUserCache } from './../VaultUserCache'
import { SharedVaultUserServerHash, getErrorFromErrorResponse, isErrorResponse } from '@standardnotes/responses'
import { SharedVaultUsersServerInterface } from '@standardnotes/api'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
export class GetVaultUsers {
constructor(private vaultUsersServer: SharedVaultUsersServerInterface) {}
export class GetVaultUsers implements UseCaseInterface<SharedVaultUserServerHash[]> {
constructor(private vaultUsersServer: SharedVaultUsersServerInterface, private cache: VaultUserCache) {}
async execute(params: {
sharedVaultUuid: string
readFromCache: boolean
}): Promise<Result<SharedVaultUserServerHash[]>> {
if (params.readFromCache) {
const cachedUsers = this.cache.get(params.sharedVaultUuid)
if (cachedUsers) {
return Result.ok(cachedUsers)
}
}
async execute(params: { sharedVaultUuid: string }): Promise<SharedVaultUserServerHash[] | undefined> {
const response = await this.vaultUsersServer.getSharedVaultUsers({ sharedVaultUuid: params.sharedVaultUuid })
if (isErrorResponse(response)) {
return undefined
return Result.fail(getErrorFromErrorResponse(response).message)
}
return response.data.users
this.cache.set(params.sharedVaultUuid, response.data.users)
return Result.ok(response.data.users)
}
}

View File

@ -1,7 +1,7 @@
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
import { SharedVaultListingInterface } from '@standardnotes/models'
export class IsVaultAdmin implements SyncUseCaseInterface<boolean> {
export class IsVaultOwner implements SyncUseCaseInterface<boolean> {
execute(dto: { sharedVault: SharedVaultListingInterface; userUuid: string }): Result<boolean> {
if (!dto.sharedVault.sharing.ownerUserUuid) {
throw new Error(`Shared vault ${dto.sharedVault.sharing.sharedVaultUuid} does not have an owner user uuid`)

View File

@ -0,0 +1,15 @@
import { SharedVaultUserServerHash } from '@standardnotes/responses'
type SharedVaultUuid = string
export class VaultUserCache {
private cache = new Map<SharedVaultUuid, SharedVaultUserServerHash[]>()
public get(sharedVaultUuid: SharedVaultUuid): SharedVaultUserServerHash[] | undefined {
return this.cache.get(sharedVaultUuid)
}
public set(sharedVaultUuid: SharedVaultUuid, users: SharedVaultUserServerHash[]): void {
this.cache.set(sharedVaultUuid, users)
}
}

View File

@ -11,7 +11,7 @@ import { ClientDisplayableError, SharedVaultUserServerHash, isClientDisplayableE
import { AbstractService } from './../Service/AbstractService'
import { VaultUserServiceEvent } from './VaultUserServiceEvent'
import { Result } from '@standardnotes/domain-core'
import { IsVaultAdmin } from './UseCase/IsVaultAdmin'
import { IsVaultOwner } from './UseCase/IsVaultOwner'
export class VaultUserService extends AbstractService<VaultUserServiceEvent> implements VaultUserServiceInterface {
constructor(
@ -19,7 +19,7 @@ export class VaultUserService extends AbstractService<VaultUserServiceEvent> imp
private vaults: VaultServiceInterface,
private _getVaultUsers: GetVaultUsers,
private _removeVaultMember: RemoveVaultMember,
private _isVaultAdmin: IsVaultAdmin,
private _isVaultOwner: IsVaultOwner,
private _getVault: GetVault,
private _leaveVault: LeaveVault,
eventBus: InternalEventBusInterface,
@ -33,7 +33,7 @@ export class VaultUserService extends AbstractService<VaultUserServiceEvent> imp
;(this.vaults as unknown) = undefined
;(this._getVaultUsers as unknown) = undefined
;(this._removeVaultMember as unknown) = undefined
;(this._isVaultAdmin as unknown) = undefined
;(this._isVaultOwner as unknown) = undefined
;(this._getVault as unknown) = undefined
;(this._leaveVault as unknown) = undefined
}
@ -41,11 +41,19 @@ export class VaultUserService extends AbstractService<VaultUserServiceEvent> imp
public async getSharedVaultUsers(
sharedVault: SharedVaultListingInterface,
): Promise<SharedVaultUserServerHash[] | undefined> {
return this._getVaultUsers.execute({ sharedVaultUuid: sharedVault.sharing.sharedVaultUuid })
const result = await this._getVaultUsers.execute({
sharedVaultUuid: sharedVault.sharing.sharedVaultUuid,
readFromCache: false,
})
if (result.isFailed()) {
return undefined
}
return result.getValue()
}
public isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean {
return this._isVaultAdmin
return this._isVaultOwner
.execute({
sharedVault,
userUuid: this.session.userUuid,

View File

@ -20,7 +20,7 @@ export class GetVault implements SyncUseCaseInterface<VaultListingInterface> {
} else {
const result = vaults.find((listing) => listing.sharing?.sharedVaultUuid === query.sharedVaultUuid) as T
if (!result) {
return Result.fail('Vault not found')
return Result.fail('Shared vault not found')
}
return Result.ok(result)

View File

@ -0,0 +1,15 @@
import { ContentType, Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
import { VaultListingInterface } from '@standardnotes/models'
import { ItemManagerInterface } from '../../Item/ItemManagerInterface'
export class GetVaults implements SyncUseCaseInterface<VaultListingInterface[]> {
constructor(private items: ItemManagerInterface) {}
execute(): Result<VaultListingInterface[]> {
const vaults = this.items.getItems<VaultListingInterface>(ContentType.TYPES.VaultListing).sort((a, b) => {
return a.name.localeCompare(b.name)
})
return Result.ok(vaults)
}
}

View File

@ -28,6 +28,7 @@ import { AlertService } from '../Alert/AlertService'
import { ContentType } from '@standardnotes/domain-core'
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
import { KeySystemKeyManagerInterface } from '../KeySystem/KeySystemKeyManagerInterface'
import { GetVaults } from './UseCase/GetVaults'
export class VaultService
extends AbstractService<VaultServiceEvent, VaultServiceEventPayload[VaultServiceEvent]>
@ -43,11 +44,12 @@ export class VaultService
private keys: KeySystemKeyManagerInterface,
private alerts: AlertService,
private _getVault: GetVault,
private _getVaults: GetVaults,
private _changeVaultKeyOptions: ChangeVaultKeyOptions,
private _moveItemsToVault: MoveItemsToVault,
private _createVault: CreateVault,
private _removeItemFromVaultUseCase: RemoveItemFromVault,
private _deleteVaultUseCase: DeleteVault,
private _removeItemFromVault: RemoveItemFromVault,
private _deleteVaultUse: DeleteVault,
private _rotateVaultKey: RotateVaultKey,
eventBus: InternalEventBusInterface,
) {
@ -62,9 +64,7 @@ export class VaultService
}
getVaults(): VaultListingInterface[] {
return this.items.getItems<VaultListingInterface>(ContentType.TYPES.VaultListing).sort((a, b) => {
return a.name.localeCompare(b.name)
})
return this._getVaults.execute().getValue()
}
getLockedvaults(): VaultListingInterface[] {
@ -166,7 +166,7 @@ export class VaultService
throw new Error('Attempting to remove item from locked vault')
}
await this._removeItemFromVaultUseCase.execute({ item })
await this._removeItemFromVault.execute({ item })
return this.items.findSureItem(item.uuid)
}
@ -175,7 +175,7 @@ export class VaultService
throw new Error('Shared vault must be deleted through SharedVaultService')
}
const error = await this._deleteVaultUseCase.execute(vault)
const error = await this._deleteVaultUse.execute(vault)
if (isClientDisplayableError(error)) {
return false
@ -328,11 +328,12 @@ export class VaultService
;(this.encryption as unknown) = undefined
;(this.alerts as unknown) = undefined
;(this._getVault as unknown) = undefined
;(this._getVaults as unknown) = undefined
;(this._changeVaultKeyOptions as unknown) = undefined
;(this._moveItemsToVault as unknown) = undefined
;(this._createVault as unknown) = undefined
;(this._removeItemFromVaultUseCase as unknown) = undefined
;(this._deleteVaultUseCase as unknown) = undefined
;(this._removeItemFromVault as unknown) = undefined
;(this._deleteVaultUse as unknown) = undefined
;(this._rotateVaultKey as unknown) = undefined
this.lockMap.clear()

View File

@ -36,6 +36,7 @@ export * from './Contacts/ContactService'
export * from './Contacts/ContactServiceInterface'
export * from './Contacts/SelfContactManager'
export * from './Contacts/UseCase/CreateOrEditContact'
export * from './Contacts/UseCase/DeleteContact'
export * from './Contacts/UseCase/EditContact'
export * from './Contacts/UseCase/FindContact'
export * from './Contacts/UseCase/GetAllContacts'
@ -133,10 +134,13 @@ export * from './Session/UserKeyPairChangedEventData'
export * from './SharedVaults/SharedVaultService'
export * from './SharedVaults/SharedVaultServiceEvent'
export * from './SharedVaults/SharedVaultServiceInterface'
export * from './SharedVaults/UseCase/ContactBelongsToVault'
export * from './SharedVaults/UseCase/ConvertToSharedVault'
export * from './SharedVaults/UseCase/CreateSharedVault'
export * from './SharedVaults/UseCase/DeleteExternalSharedVault'
export * from './SharedVaults/UseCase/DeleteSharedVault'
export * from './SharedVaults/UseCase/GetOwnedSharedVaults'
export * from './SharedVaults/UseCase/GetSharedVaults'
export * from './SharedVaults/UseCase/NotifyVaultUsersOfKeyRotation'
export * from './SharedVaults/UseCase/SendVaultDataChangedMessage'
export * from './SharedVaults/UseCase/SendVaultKeyChangedMessage'
@ -188,6 +192,7 @@ export * from './Vaults/UseCase/ChangeVaultKeyOptions'
export * from './Vaults/UseCase/CreateVault'
export * from './Vaults/UseCase/DeleteVault'
export * from './Vaults/UseCase/GetVault'
export * from './Vaults/UseCase/GetVaults'
export * from './Vaults/UseCase/MoveItemsToVault'
export * from './Vaults/UseCase/RemoveItemFromVault'
export * from './Vaults/UseCase/RotateVaultKey'
@ -197,9 +202,10 @@ export * from './Vaults/VaultServiceInterface'
export * from './VaultUser/UseCase/GetVaultContacts'
export * from './VaultUser/UseCase/GetVaultContacts'
export * from './VaultUser/UseCase/GetVaultUsers'
export * from './VaultUser/UseCase/IsVaultAdmin'
export * from './VaultUser/UseCase/IsVaultOwner'
export * from './VaultUser/UseCase/LeaveSharedVault'
export * from './VaultUser/UseCase/RemoveSharedVaultMember'
export * from './VaultUser/VaultUserCache'
export * from './VaultUser/VaultUserService'
export * from './VaultUser/VaultUserServiceEvent'
export * from './VaultUser/VaultUserServiceInterface'

View File

@ -109,8 +109,14 @@ import {
ItemsEncryptionService,
DecryptBackupFile,
VaultUserService,
IsVaultAdmin,
IsVaultOwner,
VaultInviteService,
VaultUserCache,
GetVaults,
GetSharedVaults,
GetOwnedSharedVaults,
ContactBelongsToVault,
DeleteContact,
} from '@standardnotes/services'
import { ItemManager } from '../../Services/Items/ItemManager'
import { PayloadManager } from '../../Services/Payloads/PayloadManager'
@ -207,8 +213,8 @@ export class Dependencies {
)
})
this.factory.set(TYPES.IsVaultAdmin, () => {
return new IsVaultAdmin()
this.factory.set(TYPES.IsVaultOwner, () => {
return new IsVaultOwner()
})
this.factory.set(TYPES.DecryptBackupFile, () => {
@ -223,6 +229,15 @@ export class Dependencies {
return new FindContact(this.get(TYPES.ItemManager))
})
this.factory.set(TYPES.DeleteContact, () => {
return new DeleteContact(
this.get(TYPES.MutatorService),
this.get(TYPES.SyncService),
this.get(TYPES.GetOwnedSharedVaults),
this.get(TYPES.ContactBelongsToVault),
)
})
this.factory.set(TYPES.EditContact, () => {
return new EditContact(this.get(TYPES.MutatorService), this.get(TYPES.SyncService))
})
@ -248,6 +263,22 @@ export class Dependencies {
return new GetVault(this.get(TYPES.ItemManager))
})
this.factory.set(TYPES.GetVaults, () => {
return new GetVaults(this.get(TYPES.ItemManager))
})
this.factory.set(TYPES.GetSharedVaults, () => {
return new GetSharedVaults(this.get(TYPES.GetVaults))
})
this.factory.set(TYPES.GetOwnedSharedVaults, () => {
return new GetOwnedSharedVaults(this.get(TYPES.GetSharedVaults), this.get(TYPES.IsVaultOwner))
})
this.factory.set(TYPES.ContactBelongsToVault, () => {
return new ContactBelongsToVault(this.get(TYPES.GetVaultUsers))
})
this.factory.set(TYPES.ChangeVaultKeyOptions, () => {
return new ChangeVaultKeyOptions(
this.get(TYPES.MutatorService),
@ -453,7 +484,7 @@ export class Dependencies {
})
this.factory.set(TYPES.GetVaultUsers, () => {
return new GetVaultUsers(this.get(TYPES.SharedVaultUsersServer))
return new GetVaultUsers(this.get(TYPES.SharedVaultUsersServer), this.get(TYPES.VaultUserCache))
})
this.factory.set(TYPES.DecryptOwnMessage, () => {
@ -621,13 +652,17 @@ export class Dependencies {
this.get(TYPES.VaultService),
this.get(TYPES.GetVaultUsers),
this.get(TYPES.RemoveVaultMember),
this.get(TYPES.IsVaultAdmin),
this.get(TYPES.IsVaultOwner),
this.get(TYPES.GetVault),
this.get(TYPES.LeaveVault),
this.get(TYPES.InternalEventBus),
)
})
this.factory.set(TYPES.VaultUserCache, () => {
return new VaultUserCache()
})
this.factory.set(TYPES.VaultInviteService, () => {
return new VaultInviteService(
this.get(TYPES.ItemManager),
@ -650,9 +685,10 @@ export class Dependencies {
this.factory.set(TYPES.AsymmetricMessageService, () => {
return new AsymmetricMessageService(
this.get(TYPES.AsymmetricMessageServer),
this.get(TYPES.EncryptionService),
this.get(TYPES.MutatorService),
this.get(TYPES.SessionManager),
this.get(TYPES.AsymmetricMessageServer),
this.get(TYPES.CreateOrEditContact),
this.get(TYPES.FindContact),
this.get(TYPES.GetAllContacts),
@ -673,8 +709,8 @@ export class Dependencies {
this.get(TYPES.ItemManager),
this.get(TYPES.EncryptionService),
this.get(TYPES.SessionManager),
this.get(TYPES.VaultService),
this.get(TYPES.GetVault),
this.get(TYPES.GetOwnedSharedVaults),
this.get(TYPES.CreateSharedVault),
this.get(TYPES.HandleKeyPairChange),
this.get(TYPES.NotifyVaultUsersOfKeyRotation),
@ -684,7 +720,7 @@ export class Dependencies {
this.get(TYPES.ShareContactWithVault),
this.get(TYPES.ConvertToSharedVault),
this.get(TYPES.DeleteSharedVault),
this.get(TYPES.IsVaultAdmin),
this.get(TYPES.IsVaultOwner),
this.get(TYPES.InternalEventBus),
)
})
@ -698,6 +734,7 @@ export class Dependencies {
this.get(TYPES.KeySystemKeyManager),
this.get(TYPES.AlertService),
this.get(TYPES.GetVault),
this.get(TYPES.GetVaults),
this.get(TYPES.ChangeVaultKeyOptions),
this.get(TYPES.MoveItemsToVault),
this.get(TYPES.CreateVault),
@ -727,6 +764,7 @@ export class Dependencies {
this.get(TYPES.UserService),
this.get(TYPES.SelfContactManager),
this.get(TYPES.EncryptionService),
this.get(TYPES.DeleteContact),
this.get(TYPES.FindContact),
this.get(TYPES.GetAllContacts),
this.get(TYPES.CreateOrEditContact),

View File

@ -61,6 +61,7 @@ export const TYPES = {
ItemsEncryptionService: Symbol.for('ItemsEncryptionService'),
VaultUserService: Symbol.for('VaultUserService'),
VaultInviteService: Symbol.for('VaultInviteService'),
VaultUserCache: Symbol.for('VaultUserCache'),
// Servers
RevisionServer: Symbol.for('RevisionServer'),
@ -94,9 +95,14 @@ export const TYPES = {
EditContact: Symbol.for('EditContact'),
ValidateItemSigner: Symbol.for('ValidateItemSigner'),
GetVault: Symbol.for('GetVault'),
GetVaults: Symbol.for('GetVaults'),
GetSharedVaults: Symbol.for('GetSharedVaults'),
GetOwnedSharedVaults: Symbol.for('GetOwnedSharedVaults'),
ChangeVaultKeyOptions: Symbol.for('ChangeVaultKeyOptions'),
MoveItemsToVault: Symbol.for('MoveItemsToVault'),
CreateVault: Symbol.for('CreateVault'),
DeleteContact: Symbol.for('DeleteContact'),
ContactBelongsToVault: Symbol.for('ContactBelongsToVault'),
RemoveItemFromVault: Symbol.for('RemoveItemFromVault'),
DeleteVault: Symbol.for('DeleteVault'),
RotateVaultKey: Symbol.for('RotateVaultKey'),
@ -142,7 +148,7 @@ export const TYPES = {
EncryptTypeAPayload: Symbol.for('EncryptTypeAPayload'),
EncryptTypeAPayloadWithKeyLookup: Symbol.for('EncryptTypeAPayloadWithKeyLookup'),
DecryptBackupFile: Symbol.for('DecryptBackupFile'),
IsVaultAdmin: Symbol.for('IsVaultAdmin'),
IsVaultOwner: Symbol.for('IsVaultOwner'),
// Mappers
SessionStorageMapper: Symbol.for('SessionStorageMapper'),

View File

@ -1,18 +1,21 @@
export const VaultTests = [
'vaults/vaults.test.js',
'vaults/pkc.test.js',
'vaults/contacts.test.js',
'vaults/crypto.test.js',
'vaults/asymmetric-messages.test.js',
'vaults/keypair-change.test.js',
'vaults/signatures.test.js',
'vaults/shared_vaults.test.js',
'vaults/invites.test.js',
'vaults/items.test.js',
'vaults/conflicts.test.js',
'vaults/deletion.test.js',
'vaults/permissions.test.js',
'vaults/key_rotation.test.js',
'vaults/files.test.js',
];
export const VaultTests = {
enabled: false,
exclusive: false,
files: [
'vaults/vaults.test.js',
'vaults/pkc.test.js',
'vaults/contacts.test.js',
'vaults/crypto.test.js',
'vaults/asymmetric-messages.test.js',
'vaults/keypair-change.test.js',
'vaults/signatures.test.js',
'vaults/shared_vaults.test.js',
'vaults/invites.test.js',
'vaults/items.test.js',
'vaults/conflicts.test.js',
'vaults/deletion.test.js',
'vaults/permissions.test.js',
'vaults/key_rotation.test.js',
'vaults/files.test.js',
],
}

View File

@ -578,9 +578,11 @@ export class AppContext {
}
async changeNoteTitle(note, title) {
return this.application.mutator.changeNote(note, (mutator) => {
await this.application.mutator.changeNote(note, (mutator) => {
mutator.title = title
})
return this.findItem(note.uuid)
}
async changeNoteTitleAndSync(note, title) {

View File

@ -38,10 +38,6 @@
<script type="module">
import MainRegistry from './TestRegistry/MainRegistry.js'
const InternalFeatureStatus = {
[InternalFeature.Vaults]: { enabled: false, exclusive: false },
}
const loadTest = (fileName) => {
return new Promise((resolve) => {
const script = document.createElement('script');
@ -60,12 +56,12 @@
}
}
if (InternalFeatureStatus[InternalFeature.Vaults].enabled) {
if (MainRegistry.VaultTests.enabled) {
InternalFeatureService.get().enableFeature(InternalFeature.Vaults);
await loadTests(MainRegistry.VaultTests);
await loadTests(MainRegistry.VaultTests.files);
}
if (!InternalFeatureStatus[InternalFeature.Vaults].exclusive) {
if (!MainRegistry.VaultTests.enabled.exclusive) {
await loadTests(MainRegistry.BaseTests);
}
@ -77,4 +73,4 @@
<div id="mocha"></div>
</body>
</html>
</html>

View File

@ -27,7 +27,32 @@ describe('asymmetric messages', function () {
})
it('should not trust message if the trusted payload data recipientUuid does not match the message user uuid', async () => {
console.error('TODO: implement')
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
contactContext.lockSyncing()
await context.vaults.changeVaultNameAndDescription(sharedVault, {
name: 'new vault name',
description: 'new vault description',
})
Object.defineProperty(contactContext.asymmetric.sessions, 'userUuid', {
get: () => 'invalid user uuid',
})
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
contactContext.unlockSyncing()
await contactContext.sync()
await completedProcessingMessagesPromise
const updatedVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
expect(updatedVault.name).to.not.equal('new vault name')
expect(updatedVault.description).to.not.equal('new vault description')
await deinitContactContext()
})
it('should delete message after processing it', async () => {
@ -89,7 +114,7 @@ describe('asymmetric messages', function () {
await contactContext.sync()
await completedProcessingMessagesPromise
const updatedContact = contactContext.contacts.findTrustedContact(thirdPartyContext.userUuid)
const updatedContact = contactContext.contacts.findContact(thirdPartyContext.userUuid)
expect(updatedContact.name).to.equal('Changed 3rd Party Name')
await deinitContactContext()
@ -228,7 +253,7 @@ describe('asymmetric messages', function () {
expect(firstPartySpy.callCount).to.equal(0)
expect(secondPartySpy.callCount).to.equal(1)
const contact = contactContext.contacts.findTrustedContact(context.userUuid)
const contact = contactContext.contacts.findContact(context.userUuid)
expect(contact.publicKeySet.encryption).to.equal(context.publicKey)
expect(contact.publicKeySet.signing).to.equal(context.signingPublicKey)
@ -294,7 +319,7 @@ describe('asymmetric messages', function () {
const { contactContext, deinitContactContext } = await Collaboration.createContactContext()
await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
await Collaboration.createTrustedContactForUserOfContext(contactContext, context)
const originalContact = contactContext.contacts.findTrustedContact(context.userUuid)
const originalContact = contactContext.contacts.findContact(context.userUuid)
await context.changePassword('new_password')
@ -302,7 +327,7 @@ describe('asymmetric messages', function () {
await contactContext.sync()
await completedProcessingMessagesPromise
const updatedContact = contactContext.contacts.findTrustedContact(context.userUuid)
const updatedContact = contactContext.contacts.findContact(context.userUuid)
expect(updatedContact.publicKeySet.encryption).to.not.equal(originalContact.publicKeySet.encryption)
expect(updatedContact.publicKeySet.signing).to.not.equal(originalContact.publicKeySet.signing)
@ -352,7 +377,7 @@ describe('asymmetric messages', function () {
await contactContext.sync()
await completedProcessingMessagesPromise
const updatedContact = contactContext.contacts.findTrustedContact(context.userUuid)
const updatedContact = contactContext.contacts.findContact(context.userUuid)
expect(updatedContact.publicKeySet.encryption).to.equal(newKeyPair.publicKey)
expect(updatedContact.publicKeySet.signing).to.equal(newSigningKeyPair.publicKey)

View File

@ -1,4 +1,5 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
@ -90,7 +91,14 @@ describe('contacts', function () {
})
it('should not be able to delete a trusted contact if it belongs to a vault I administer', async () => {
console.error('TODO: implement test')
const { contact, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context)
const result = await context.contacts.deleteContact(contact)
expect(result.isFailed()).to.be.true
expect(result.getError()).to.equal('Cannot delete contact that belongs to an owned vault')
await deinitContactContext()
})
it('should be able to refresh a contact using a collaborationID that includes full chain of previouos public keys', async () => {

View File

@ -167,11 +167,11 @@ describe('shared vault crypto', function () {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
expect(context.contacts.isItemAuthenticallySigned(note)).to.equal(ItemSignatureValidationResult.NotApplicable)
expect(context.contacts.getItemSignatureStatus(note)).to.equal(ItemSignatureValidationResult.NotApplicable)
const contactNote = contactContext.items.findItem(note.uuid)
expect(contactContext.contacts.isItemAuthenticallySigned(contactNote)).to.equal(
expect(contactContext.contacts.getItemSignatureStatus(contactNote)).to.equal(
ItemSignatureValidationResult.Trusted,
)
@ -181,7 +181,7 @@ describe('shared vault crypto', function () {
let updatedNote = context.items.findItem(note.uuid)
expect(context.contacts.isItemAuthenticallySigned(updatedNote)).to.equal(ItemSignatureValidationResult.Trusted)
expect(context.contacts.getItemSignatureStatus(updatedNote)).to.equal(ItemSignatureValidationResult.Trusted)
await deinitContactContext()
})

View File

@ -23,11 +23,106 @@ describe('signatures', function () {
await context.register()
})
it('signatures should be marked as of questionable integrity when signed with non root contact public key', async () => {
console.error('TODO: implement test')
describe('item decryption signature verification', () => {
it('should have failing signature if contact public key does not match', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
const regularContact = contactContext.contacts.findContact(context.userUuid)
const decoyContact = new TrustedContact(
regularContact.payload.copy({
content: {
...regularContact.payload.content,
publicKeySet: ContactPublicKeySet.FromJson({
...regularContact.payload.content.publicKeySet,
encryption: 'invalid public key',
signing: 'invalid signing public key',
}),
},
}),
)
contactContext.items.collection.onChange({
changed: [decoyContact],
inserted: [],
discarded: [],
ignored: [],
unerrored: [],
})
await context.changeNoteTitle(note, 'new title')
await contactContext.sync()
const contactNote = contactContext.items.findItem(note.uuid)
/** Signature data only verifies whether the embedded signature and embedded signature public key match up */
expect(contactNote.signatureData.required).to.be.true
expect(contactNote.signatureData.result.passes).to.be.true
const status = contactContext.contacts.getItemSignatureStatus(contactNote)
expect(status).to.equal(ItemSignatureValidationResult.NotTrusted)
await deinitContactContext()
})
})
it('items marked with questionable integrity should have option to trust the item which would resync it', async () => {
console.error('TODO: implement test')
describe('UI signature status check', () => {
it('signatures should be trusted with root public key', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
const contactNote = contactContext.items.findItem(note.uuid)
const status = contactContext.contacts.getItemSignatureStatus(contactNote)
expect(status).to.equal(ItemSignatureValidationResult.Trusted)
await deinitContactContext()
})
it('signatures return SignedWithNonCurrentKey when signed with non root contact public key', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
await context.changePassword('new password')
await contactContext.sync()
const contactNote = contactContext.items.findItem(note.uuid)
const status = contactContext.contacts.getItemSignatureStatus(contactNote)
expect(status).to.equal(ItemSignatureValidationResult.SignedWithNonCurrentKey)
await deinitContactContext()
})
it('syncing a SignedWithNonCurrentKey item should reset its status', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
await context.changePassword('new password')
await contactContext.sync()
const contactNote = contactContext.items.findItem(note.uuid)
const latestNote = await contactContext.changeNoteTitle(contactNote, 'new title')
const status = contactContext.contacts.getItemSignatureStatus(latestNote)
expect(status).to.equal(ItemSignatureValidationResult.NotApplicable)
await deinitContactContext()
})
it('should return NotApplicable if item does not belong to shared vault', async () => {
const item = await context.createSyncedNote()
const status = context.contacts.getItemSignatureStatus(item)
expect(status).to.equal(ItemSignatureValidationResult.NotApplicable)
})
})
})

View File

@ -30,7 +30,7 @@ const EditContactModal: FunctionComponent<Props> = ({ onCloseDialog, fromInvite,
useEffect(() => {
if (editContactUuid) {
const contact = application.contacts.findTrustedContact(editContactUuid)
const contact = application.contacts.findContact(editContactUuid)
if (!contact) {
throw new Error(`Contact with uuid ${editContactUuid} not found`)
}

View File

@ -29,7 +29,7 @@ const InviteItem = ({ inviteRecord }: Props) => {
const closeAddContactModal = () => setIsAddContactModalOpen(false)
const collaborationId = application.contacts.getCollaborationIDFromInvite(inviteRecord.invite)
const trustedContact = application.contacts.findTrustedContactForInvite(inviteRecord.invite)
const trustedContact = application.contacts.findContactForInvite(inviteRecord.invite)
return (
<>

View File

@ -27,7 +27,7 @@ export const VaultModalInvites = ({
<div className="mb-3">
<div className="mb-3 text-lg">Pending Invites</div>
{invites.map((invite) => {
const contact = application.contacts.findTrustedContactForInvite(invite)
const contact = application.contacts.findContactForInvite(invite)
return (
<div key={invite.uuid} className="bg-gray-100 flex flex-row gap-3.5 rounded-lg px-3.5 py-2.5 shadow-md">
<Icon type={'user'} size="custom" className="mt-2.5 h-5.5 w-5.5 flex-shrink-0" />

View File

@ -35,7 +35,7 @@ export const VaultModalMembers = ({
return null
}
const contact = application.contacts.findTrustedContactForServerUser(member)
const contact = application.contacts.findContactForServerUser(member)
return (
<div
key={contact?.uuid || member.user_uuid}