tests: fix memory leaks (#2389)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,31 +1,37 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* eslint-disable no-undef */
|
||||
import { BaseItemCounts } from '../lib/BaseItemCounts.js'
|
||||
import * as Factory from '../lib/factory.js'
|
||||
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
|
||||
describe('sync integrity', () => {
|
||||
before(function () {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
after(function () {
|
||||
localStorage.clear()
|
||||
})
|
||||
let application
|
||||
let email
|
||||
let password
|
||||
let expectedItemCount
|
||||
|
||||
beforeEach(async function () {
|
||||
this.expectedItemCount = BaseItemCounts.DefaultItemsWithAccount
|
||||
this.application = await Factory.createInitAppWithFakeCrypto()
|
||||
this.email = UuidGenerator.GenerateUuid()
|
||||
this.password = UuidGenerator.GenerateUuid()
|
||||
localStorage.clear()
|
||||
expectedItemCount = BaseItemCounts.DefaultItemsWithAccount
|
||||
application = await Factory.createInitAppWithFakeCrypto()
|
||||
email = UuidGenerator.GenerateUuid()
|
||||
password = UuidGenerator.GenerateUuid()
|
||||
await Factory.registerUserToApplication({
|
||||
application: this.application,
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
application: application,
|
||||
email: email,
|
||||
password: password,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async function () {
|
||||
expect(application.sync.isOutOfSync()).to.equal(false)
|
||||
const rawPayloads = await application.storage.getAllRawPayloads()
|
||||
expect(rawPayloads.length).to.equal(expectedItemCount)
|
||||
await Factory.safeDeinit(application)
|
||||
localStorage.clear()
|
||||
application = undefined
|
||||
})
|
||||
|
||||
const awaitSyncEventPromise = (application, targetEvent) => {
|
||||
return new Promise((resolve) => {
|
||||
application.sync.addEventObserver((event) => {
|
||||
@@ -36,44 +42,37 @@ describe('sync integrity', () => {
|
||||
})
|
||||
}
|
||||
|
||||
afterEach(async function () {
|
||||
expect(this.application.sync.isOutOfSync()).to.equal(false)
|
||||
const rawPayloads = await this.application.storage.getAllRawPayloads()
|
||||
expect(rawPayloads.length).to.equal(this.expectedItemCount)
|
||||
await Factory.safeDeinit(this.application)
|
||||
})
|
||||
|
||||
it('should detect when out of sync', async function () {
|
||||
const item = await this.application.mutator.emitItemFromPayload(
|
||||
const item = await application.mutator.emitItemFromPayload(
|
||||
Factory.createNotePayload(),
|
||||
PayloadEmitSource.LocalChanged,
|
||||
)
|
||||
this.expectedItemCount++
|
||||
expectedItemCount++
|
||||
|
||||
const didEnterOutOfSync = awaitSyncEventPromise(this.application, SyncEvent.EnterOutOfSync)
|
||||
await this.application.sync.sync({ checkIntegrity: true })
|
||||
const didEnterOutOfSync = awaitSyncEventPromise(application, SyncEvent.EnterOutOfSync)
|
||||
await application.sync.sync({ checkIntegrity: true })
|
||||
|
||||
await this.application.items.removeItemFromMemory(item)
|
||||
await this.application.sync.sync({ checkIntegrity: true, awaitAll: true })
|
||||
await application.items.removeItemFromMemory(item)
|
||||
await application.sync.sync({ checkIntegrity: true, awaitAll: true })
|
||||
|
||||
await didEnterOutOfSync
|
||||
})
|
||||
|
||||
it('should self heal after out of sync', async function () {
|
||||
const item = await this.application.mutator.emitItemFromPayload(
|
||||
const item = await application.mutator.emitItemFromPayload(
|
||||
Factory.createNotePayload(),
|
||||
PayloadEmitSource.LocalChanged,
|
||||
)
|
||||
this.expectedItemCount++
|
||||
expectedItemCount++
|
||||
|
||||
const didEnterOutOfSync = awaitSyncEventPromise(this.application, SyncEvent.EnterOutOfSync)
|
||||
const didExitOutOfSync = awaitSyncEventPromise(this.application, SyncEvent.ExitOutOfSync)
|
||||
const didEnterOutOfSync = awaitSyncEventPromise(application, SyncEvent.EnterOutOfSync)
|
||||
const didExitOutOfSync = awaitSyncEventPromise(application, SyncEvent.ExitOutOfSync)
|
||||
|
||||
await this.application.sync.sync({ checkIntegrity: true })
|
||||
await this.application.items.removeItemFromMemory(item)
|
||||
await this.application.sync.sync({ checkIntegrity: true, awaitAll: true })
|
||||
await application.sync.sync({ checkIntegrity: true })
|
||||
await application.items.removeItemFromMemory(item)
|
||||
await application.sync.sync({ checkIntegrity: true, awaitAll: true })
|
||||
|
||||
await Promise.all([didEnterOutOfSync, didExitOutOfSync])
|
||||
expect(this.application.sync.isOutOfSync()).to.equal(false)
|
||||
expect(application.sync.isOutOfSync()).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* eslint-disable no-undef */
|
||||
import * as Factory from '../lib/factory.js'
|
||||
import { createRelatedNoteTagPairPayload } from '../lib/Items.js'
|
||||
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
|
||||
describe('notes + tags syncing', function () {
|
||||
let application
|
||||
|
||||
const syncOptions = {
|
||||
checkIntegrity: true,
|
||||
awaitAll: true,
|
||||
@@ -16,30 +17,31 @@ describe('notes + tags syncing', function () {
|
||||
})
|
||||
|
||||
beforeEach(async function () {
|
||||
this.application = await Factory.createInitAppWithFakeCrypto()
|
||||
Factory.disableIntegrityAutoHeal(this.application)
|
||||
application = await Factory.createInitAppWithFakeCrypto()
|
||||
Factory.disableIntegrityAutoHeal(application)
|
||||
const email = UuidGenerator.GenerateUuid()
|
||||
const password = UuidGenerator.GenerateUuid()
|
||||
await Factory.registerUserToApplication({
|
||||
application: this.application,
|
||||
application: application,
|
||||
email,
|
||||
password,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async function () {
|
||||
await Factory.safeDeinit(this.application)
|
||||
await Factory.safeDeinit(application)
|
||||
application = undefined
|
||||
})
|
||||
|
||||
it('syncing an item then downloading it should include items_key_id', async function () {
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
await this.application.sync.sync(syncOptions)
|
||||
await this.application.payloads.resetState()
|
||||
await this.application.items.resetState()
|
||||
await this.application.sync.clearSyncPositionTokens()
|
||||
await this.application.sync.sync(syncOptions)
|
||||
const downloadedNote = this.application.items.getDisplayableNotes()[0]
|
||||
const note = await Factory.createMappedNote(application)
|
||||
await application.mutator.setItemDirty(note)
|
||||
await application.sync.sync(syncOptions)
|
||||
await application.payloads.resetState()
|
||||
await application.items.resetState()
|
||||
await application.sync.clearSyncPositionTokens()
|
||||
await application.sync.sync(syncOptions)
|
||||
const downloadedNote = application.items.getDisplayableNotes()[0]
|
||||
expect(downloadedNote.items_key_id).to.not.be.ok
|
||||
// Allow time for waitingForKey
|
||||
await Factory.sleep(0.1)
|
||||
@@ -52,21 +54,21 @@ describe('notes + tags syncing', function () {
|
||||
const notePayload = pair[0]
|
||||
const tagPayload = pair[1]
|
||||
|
||||
await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
const note = this.application.items.getItems([ContentType.TYPES.Note])[0]
|
||||
const tag = this.application.items.getItems([ContentType.TYPES.Tag])[0]
|
||||
expect(this.application.items.getDisplayableNotes().length).to.equal(1)
|
||||
expect(this.application.items.getDisplayableTags().length).to.equal(1)
|
||||
await application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
const note = application.items.getItems([ContentType.TYPES.Note])[0]
|
||||
const tag = application.items.getItems([ContentType.TYPES.Tag])[0]
|
||||
expect(application.items.getDisplayableNotes().length).to.equal(1)
|
||||
expect(application.items.getDisplayableTags().length).to.equal(1)
|
||||
|
||||
for (let i = 0; i < 9; i++) {
|
||||
await this.application.mutator.setItemsDirty([note, tag])
|
||||
await this.application.sync.sync(syncOptions)
|
||||
this.application.sync.clearSyncPositionTokens()
|
||||
await application.mutator.setItemsDirty([note, tag])
|
||||
await application.sync.sync(syncOptions)
|
||||
application.sync.clearSyncPositionTokens()
|
||||
expect(tag.content.references.length).to.equal(1)
|
||||
expect(this.application.items.itemsReferencingItem(note).length).to.equal(1)
|
||||
expect(application.items.itemsReferencingItem(note).length).to.equal(1)
|
||||
expect(tag.noteCount).to.equal(1)
|
||||
expect(this.application.items.getDisplayableNotes().length).to.equal(1)
|
||||
expect(this.application.items.getDisplayableTags().length).to.equal(1)
|
||||
expect(application.items.getDisplayableNotes().length).to.equal(1)
|
||||
expect(application.items.getDisplayableTags().length).to.equal(1)
|
||||
console.warn('Waiting 0.1s...')
|
||||
await Factory.sleep(0.1)
|
||||
}
|
||||
@@ -76,59 +78,59 @@ describe('notes + tags syncing', function () {
|
||||
const pair = createRelatedNoteTagPairPayload()
|
||||
const notePayload = pair[0]
|
||||
const tagPayload = pair[1]
|
||||
await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
const originalNote = this.application.items.getDisplayableNotes()[0]
|
||||
const originalTag = this.application.items.getDisplayableTags()[0]
|
||||
await this.application.mutator.setItemsDirty([originalNote, originalTag])
|
||||
await application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
const originalNote = application.items.getDisplayableNotes()[0]
|
||||
const originalTag = application.items.getDisplayableTags()[0]
|
||||
await application.mutator.setItemsDirty([originalNote, originalTag])
|
||||
|
||||
await this.application.sync.sync(syncOptions)
|
||||
await application.sync.sync(syncOptions)
|
||||
|
||||
expect(originalTag.content.references.length).to.equal(1)
|
||||
expect(originalTag.noteCount).to.equal(1)
|
||||
expect(this.application.items.itemsReferencingItem(originalNote).length).to.equal(1)
|
||||
expect(application.items.itemsReferencingItem(originalNote).length).to.equal(1)
|
||||
|
||||
// when signing in, all local items are cleared from storage (but kept in memory; to clear desktop logs),
|
||||
// then resaved with alternated uuids.
|
||||
await this.application.storage.clearAllPayloads()
|
||||
await this.application.sync.markAllItemsAsNeedingSyncAndPersist()
|
||||
await application.storage.clearAllPayloads()
|
||||
await application.sync.markAllItemsAsNeedingSyncAndPersist()
|
||||
|
||||
expect(this.application.items.getDisplayableNotes().length).to.equal(1)
|
||||
expect(this.application.items.getDisplayableTags().length).to.equal(1)
|
||||
expect(application.items.getDisplayableNotes().length).to.equal(1)
|
||||
expect(application.items.getDisplayableTags().length).to.equal(1)
|
||||
|
||||
const note = this.application.items.getDisplayableNotes()[0]
|
||||
const tag = this.application.items.getDisplayableTags()[0]
|
||||
const note = application.items.getDisplayableNotes()[0]
|
||||
const tag = application.items.getDisplayableTags()[0]
|
||||
|
||||
expect(tag.content.references.length).to.equal(1)
|
||||
expect(note.content.references.length).to.equal(0)
|
||||
|
||||
expect(tag.noteCount).to.equal(1)
|
||||
expect(this.application.items.itemsReferencingItem(note).length).to.equal(1)
|
||||
expect(application.items.itemsReferencingItem(note).length).to.equal(1)
|
||||
})
|
||||
|
||||
it('duplicating a tag should maintian its relationships', async function () {
|
||||
const pair = createRelatedNoteTagPairPayload()
|
||||
const notePayload = pair[0]
|
||||
const tagPayload = pair[1]
|
||||
await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
let note = this.application.items.getDisplayableNotes()[0]
|
||||
let tag = this.application.items.getDisplayableTags()[0]
|
||||
expect(this.application.items.itemsReferencingItem(note).length).to.equal(1)
|
||||
await application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
let note = application.items.getDisplayableNotes()[0]
|
||||
let tag = application.items.getDisplayableTags()[0]
|
||||
expect(application.items.itemsReferencingItem(note).length).to.equal(1)
|
||||
|
||||
await this.application.mutator.setItemsDirty([note, tag])
|
||||
await this.application.sync.sync(syncOptions)
|
||||
await this.application.sync.clearSyncPositionTokens()
|
||||
await application.mutator.setItemsDirty([note, tag])
|
||||
await application.sync.sync(syncOptions)
|
||||
await application.sync.clearSyncPositionTokens()
|
||||
|
||||
note = this.application.items.findItem(note.uuid)
|
||||
tag = this.application.items.findItem(tag.uuid)
|
||||
note = application.items.findItem(note.uuid)
|
||||
tag = application.items.findItem(tag.uuid)
|
||||
|
||||
expect(note.dirty).to.equal(false)
|
||||
expect(tag.dirty).to.equal(false)
|
||||
|
||||
expect(this.application.items.getDisplayableNotes().length).to.equal(1)
|
||||
expect(this.application.items.getDisplayableTags().length).to.equal(1)
|
||||
expect(application.items.getDisplayableNotes().length).to.equal(1)
|
||||
expect(application.items.getDisplayableTags().length).to.equal(1)
|
||||
|
||||
await Factory.changePayloadTimeStampAndSync(
|
||||
this.application,
|
||||
application,
|
||||
tag.payload,
|
||||
Factory.dateToMicroseconds(Factory.yesterday()),
|
||||
{
|
||||
@@ -137,13 +139,13 @@ describe('notes + tags syncing', function () {
|
||||
syncOptions,
|
||||
)
|
||||
|
||||
tag = this.application.items.findItem(tag.uuid)
|
||||
tag = application.items.findItem(tag.uuid)
|
||||
|
||||
// tag should now be conflicted and a copy created
|
||||
expect(this.application.items.getDisplayableNotes().length).to.equal(1)
|
||||
expect(this.application.items.getDisplayableTags().length).to.equal(2)
|
||||
expect(application.items.getDisplayableNotes().length).to.equal(1)
|
||||
expect(application.items.getDisplayableTags().length).to.equal(2)
|
||||
|
||||
const tags = this.application.items.getDisplayableTags()
|
||||
const tags = application.items.getDisplayableTags()
|
||||
const conflictedTag = tags.find((tag) => {
|
||||
return !!tag.content.conflict_of
|
||||
})
|
||||
@@ -157,11 +159,11 @@ describe('notes + tags syncing', function () {
|
||||
expect(conflictedTag.content.conflict_of).to.equal(originalTag.uuid)
|
||||
expect(conflictedTag.noteCount).to.equal(originalTag.noteCount)
|
||||
|
||||
expect(this.application.items.itemsReferencingItem(conflictedTag).length).to.equal(0)
|
||||
expect(this.application.items.itemsReferencingItem(originalTag).length).to.equal(0)
|
||||
expect(application.items.itemsReferencingItem(conflictedTag).length).to.equal(0)
|
||||
expect(application.items.itemsReferencingItem(originalTag).length).to.equal(0)
|
||||
|
||||
// Two tags now link to this note
|
||||
const referencingItems = this.application.items.itemsReferencingItem(note)
|
||||
const referencingItems = application.items.itemsReferencingItem(note)
|
||||
expect(referencingItems.length).to.equal(2)
|
||||
expect(referencingItems[0]).to.not.equal(referencingItems[1])
|
||||
}).timeout(10000)
|
||||
|
||||
@@ -1,71 +1,70 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* eslint-disable no-undef */
|
||||
import { BaseItemCounts } from '../lib/BaseItemCounts.js'
|
||||
import * as Factory from '../lib/factory.js'
|
||||
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
|
||||
describe('offline syncing', () => {
|
||||
let context
|
||||
let application
|
||||
let expectedItemCount
|
||||
|
||||
const syncOptions = {
|
||||
checkIntegrity: true,
|
||||
awaitAll: true,
|
||||
}
|
||||
|
||||
beforeEach(async function () {
|
||||
this.expectedItemCount = BaseItemCounts.DefaultItems
|
||||
this.context = await Factory.createAppContext()
|
||||
await this.context.launch()
|
||||
this.application = this.context.application
|
||||
localStorage.clear()
|
||||
expectedItemCount = BaseItemCounts.DefaultItems
|
||||
context = await Factory.createAppContext()
|
||||
await context.launch()
|
||||
application = context.application
|
||||
})
|
||||
|
||||
afterEach(async function () {
|
||||
expect(this.application.sync.isOutOfSync()).to.equal(false)
|
||||
await Factory.safeDeinit(this.application)
|
||||
})
|
||||
|
||||
before(async function () {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
expect(application.sync.isOutOfSync()).to.equal(false)
|
||||
await Factory.safeDeinit(application)
|
||||
localStorage.clear()
|
||||
application = undefined
|
||||
context = undefined
|
||||
})
|
||||
|
||||
it('uuid alternation should delete original payload', async function () {
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
this.expectedItemCount++
|
||||
const note = await Factory.createMappedNote(application)
|
||||
expectedItemCount++
|
||||
|
||||
await Factory.alternateUuidForItem(this.application, note.uuid)
|
||||
await this.application.sync.sync(syncOptions)
|
||||
await Factory.alternateUuidForItem(application, note.uuid)
|
||||
await application.sync.sync(syncOptions)
|
||||
|
||||
const notes = this.application.items.getDisplayableNotes()
|
||||
const notes = application.items.getDisplayableNotes()
|
||||
expect(notes.length).to.equal(1)
|
||||
expect(notes[0].uuid).to.not.equal(note.uuid)
|
||||
|
||||
const items = this.application.items.allTrackedItems()
|
||||
expect(items.length).to.equal(this.expectedItemCount)
|
||||
const items = application.items.allTrackedItems()
|
||||
expect(items.length).to.equal(expectedItemCount)
|
||||
})
|
||||
|
||||
it('should sync item with no passcode', async function () {
|
||||
let note = await Factory.createMappedNote(this.application)
|
||||
expect(Uuids(this.application.items.getDirtyItems()).includes(note.uuid))
|
||||
let note = await Factory.createMappedNote(application)
|
||||
expect(Uuids(application.items.getDirtyItems()).includes(note.uuid))
|
||||
|
||||
await this.application.sync.sync(syncOptions)
|
||||
await application.sync.sync(syncOptions)
|
||||
|
||||
note = this.application.items.findItem(note.uuid)
|
||||
note = application.items.findItem(note.uuid)
|
||||
|
||||
/** In rare cases a sync can complete so fast that the dates are equal; this is ok. */
|
||||
expect(note.lastSyncEnd).to.be.at.least(note.lastSyncBegan)
|
||||
|
||||
this.expectedItemCount++
|
||||
expectedItemCount++
|
||||
|
||||
expect(this.application.items.getDirtyItems().length).to.equal(0)
|
||||
expect(application.items.getDirtyItems().length).to.equal(0)
|
||||
|
||||
const rawPayloads2 = await this.application.storage.getAllRawPayloads()
|
||||
expect(rawPayloads2.length).to.equal(this.expectedItemCount)
|
||||
const rawPayloads2 = await application.storage.getAllRawPayloads()
|
||||
expect(rawPayloads2.length).to.equal(expectedItemCount)
|
||||
|
||||
const itemsKeyRaw = (await Factory.getStoragePayloadsOfType(this.application, ContentType.TYPES.ItemsKey))[0]
|
||||
const noteRaw = (await Factory.getStoragePayloadsOfType(this.application, ContentType.TYPES.Note))[0]
|
||||
const itemsKeyRaw = (await Factory.getStoragePayloadsOfType(application, ContentType.TYPES.ItemsKey))[0]
|
||||
const noteRaw = (await Factory.getStoragePayloadsOfType(application, ContentType.TYPES.Note))[0]
|
||||
|
||||
/** Encrypts with default items key */
|
||||
expect(typeof noteRaw.content).to.equal('string')
|
||||
@@ -75,30 +74,30 @@ describe('offline syncing', () => {
|
||||
})
|
||||
|
||||
it('should sync item encrypted with passcode', async function () {
|
||||
await this.application.addPasscode('foobar')
|
||||
await Factory.createMappedNote(this.application)
|
||||
expect(this.application.items.getDirtyItems().length).to.equal(1)
|
||||
const rawPayloads1 = await this.application.storage.getAllRawPayloads()
|
||||
expect(rawPayloads1.length).to.equal(this.expectedItemCount)
|
||||
await application.addPasscode('foobar')
|
||||
await Factory.createMappedNote(application)
|
||||
expect(application.items.getDirtyItems().length).to.equal(1)
|
||||
const rawPayloads1 = await application.storage.getAllRawPayloads()
|
||||
expect(rawPayloads1.length).to.equal(expectedItemCount)
|
||||
|
||||
await this.application.sync.sync(syncOptions)
|
||||
this.expectedItemCount++
|
||||
await application.sync.sync(syncOptions)
|
||||
expectedItemCount++
|
||||
|
||||
expect(this.application.items.getDirtyItems().length).to.equal(0)
|
||||
const rawPayloads2 = await this.application.storage.getAllRawPayloads()
|
||||
expect(rawPayloads2.length).to.equal(this.expectedItemCount)
|
||||
expect(application.items.getDirtyItems().length).to.equal(0)
|
||||
const rawPayloads2 = await application.storage.getAllRawPayloads()
|
||||
expect(rawPayloads2.length).to.equal(expectedItemCount)
|
||||
|
||||
const payload = rawPayloads2[0]
|
||||
expect(typeof payload.content).to.equal('string')
|
||||
expect(payload.content.startsWith(this.application.encryption.getLatestVersion())).to.equal(true)
|
||||
expect(payload.content.startsWith(application.encryption.getLatestVersion())).to.equal(true)
|
||||
})
|
||||
|
||||
it('signing out while offline should succeed', async function () {
|
||||
await Factory.createMappedNote(this.application)
|
||||
this.expectedItemCount++
|
||||
await this.application.sync.sync(syncOptions)
|
||||
this.application = await Factory.signOutApplicationAndReturnNew(this.application)
|
||||
expect(this.application.sessions.isSignedIn()).to.equal(false)
|
||||
expect(this.application.sessions.getUser()).to.not.be.ok
|
||||
await Factory.createMappedNote(application)
|
||||
expectedItemCount++
|
||||
await application.sync.sync(syncOptions)
|
||||
application = await Factory.signOutApplicationAndReturnNew(application)
|
||||
expect(application.sessions.isSignedIn()).to.equal(false)
|
||||
expect(application.sessions.getUser()).to.not.be.ok
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user