2023-07-27 14:31:52 +09:00
/ *
2024-02-13 15:59:27 +00:00
* SPDX - FileCopyrightText : syuilo and misskey - project
2023-07-27 14:31:52 +09:00
* SPDX - License - Identifier : AGPL - 3.0 - only
* /
2025-03-03 08:50:34 -05:00
2021-07-10 23:14:57 +09:00
process . env . NODE_ENV = 'test' ;
import * as assert from 'assert' ;
2025-02-22 20:35:41 -05:00
import { generateKeyPair } from 'crypto' ;
2023-02-19 07:27:14 +01:00
import { Test } from '@nestjs/testing' ;
import { jest } from '@jest/globals' ;
2025-02-22 20:33:45 -05:00
import type { Config } from '@/config.js' ;
2025-02-22 20:35:41 -05:00
import type { MiLocalUser , MiRemoteUser } from '@/models/User.js' ;
2023-07-15 13:12:20 +02:00
import { ApImageService } from '@/core/activitypub/models/ApImageService.js' ;
2023-02-19 07:27:14 +01:00
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js' ;
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js' ;
2023-03-12 04:11:37 +01:00
import { ApRendererService } from '@/core/activitypub/ApRendererService.js' ;
2024-05-01 07:33:58 +00:00
import { JsonLdService } from '@/core/activitypub/JsonLdService.js' ;
import { CONTEXT } from '@/core/activitypub/misc/contexts.js' ;
2023-02-19 07:27:14 +01:00
import { GlobalModule } from '@/GlobalModule.js' ;
import { CoreModule } from '@/core/CoreModule.js' ;
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js' ;
import { LoggerService } from '@/core/LoggerService.js' ;
2024-04-28 10:53:33 +09:00
import type { IActor , IApDocument , ICollection , IObject , IPost } from '@/core/activitypub/type.js' ;
2025-02-21 22:04:36 -05:00
import { MiMeta , MiNote , MiUser , MiUserKeypair , UserProfilesRepository , UserPublickeysRepository } from '@/models/_.js' ;
2024-08-09 12:10:51 +09:00
import { DI } from '@/di-symbols.js' ;
2023-06-25 04:04:33 +02:00
import { secureRndstr } from '@/misc/secure-rndstr.js' ;
2023-07-15 13:12:20 +02:00
import { DownloadService } from '@/core/DownloadService.js' ;
2023-10-16 12:58:17 +09:00
import { genAidx } from '@/misc/id/aidx.js' ;
2025-03-03 08:50:34 -05:00
import { IdService } from '@/core/IdService.js' ;
2023-06-25 04:04:33 +02:00
import { MockResolver } from '../misc/mock-resolver.js' ;
2025-02-21 22:04:36 -05:00
import { UserKeypairService } from '@/core/UserKeypairService.js' ;
import { MemoryKVCache , RedisKVCache } from '@/misc/cache.js' ;
2025-02-22 20:35:41 -05:00
import { IdService } from '@/core/IdService.js' ;
2021-07-10 23:14:57 +09:00
2023-02-19 09:49:18 +01:00
const host = 'https://host1.test' ;
2023-07-09 01:59:44 +02:00
type NonTransientIActor = IActor & { id : string } ;
type NonTransientIPost = IPost & { id : string } ;
function createRandomActor ( { actorHost = host } = { } ) : NonTransientIActor {
2023-06-25 04:04:33 +02:00
const preferredUsername = secureRndstr ( 8 ) ;
2023-07-09 01:59:44 +02:00
const actorId = ` ${ actorHost } /users/ ${ preferredUsername . toLowerCase ( ) } ` ;
2023-02-19 09:49:18 +01:00
return {
'@context' : 'https://www.w3.org/ns/activitystreams' ,
id : actorId ,
type : 'Person' ,
preferredUsername ,
inbox : ` ${ actorId } /inbox ` ,
outbox : ` ${ actorId } /outbox ` ,
} ;
}
2023-07-09 01:59:44 +02:00
function createRandomNote ( actor : NonTransientIActor ) : NonTransientIPost {
const id = secureRndstr ( 8 ) ;
const noteId = ` ${ new URL ( actor . id ) . origin } /notes/ ${ id } ` ;
return {
id : noteId ,
type : 'Note' ,
attributedTo : actor.id ,
content : 'test test foo' ,
} ;
}
function createRandomNotes ( actor : NonTransientIActor , length : number ) : NonTransientIPost [ ] {
return new Array ( length ) . fill ( null ) . map ( ( ) = > createRandomNote ( actor ) ) ;
}
function createRandomFeaturedCollection ( actor : NonTransientIActor , length : number ) : ICollection {
const items = createRandomNotes ( actor , length ) ;
return {
'@context' : 'https://www.w3.org/ns/activitystreams' ,
type : 'Collection' ,
id : actor.outbox as string ,
totalItems : items.length ,
items ,
} ;
}
2023-07-15 13:12:20 +02:00
async function createRandomRemoteUser (
resolver : MockResolver ,
personService : ApPersonService ,
2023-09-05 17:02:14 +09:00
) : Promise < MiRemoteUser > {
2023-07-15 13:12:20 +02:00
const actor = createRandomActor ( ) ;
resolver . register ( actor . id , actor ) ;
return await personService . createPerson ( actor . id , resolver ) ;
}
2021-07-10 23:14:57 +09:00
describe ( 'ActivityPub' , ( ) = > {
2024-08-09 12:10:51 +09:00
let userProfilesRepository : UserProfilesRepository ;
2023-07-15 13:12:20 +02:00
let imageService : ApImageService ;
2023-02-19 07:27:14 +01:00
let noteService : ApNoteService ;
let personService : ApPersonService ;
2023-03-12 04:11:37 +01:00
let rendererService : ApRendererService ;
2024-05-01 07:33:58 +00:00
let jsonLdService : JsonLdService ;
2023-02-19 07:27:14 +01:00
let resolver : MockResolver ;
2025-02-12 14:13:00 -05:00
let idService : IdService ;
2025-02-22 12:02:16 -05:00
let userPublickeysRepository : UserPublickeysRepository ;
2025-02-21 22:04:36 -05:00
let userKeypairService : UserKeypairService ;
2025-02-22 20:33:45 -05:00
let config : Config ;
2023-02-19 07:27:14 +01:00
2023-07-15 13:12:20 +02:00
const metaInitial = {
cacheRemoteFiles : true ,
cacheRemoteSensitiveFiles : true ,
2023-10-23 15:17:25 +09:00
enableFanoutTimeline : true ,
2023-11-16 10:20:57 +09:00
enableFanoutTimelineDbFallback : true ,
2023-10-14 02:19:28 +02:00
perUserHomeTimelineCacheMax : 800 ,
perLocalUserUserTimelineCacheMax : 800 ,
perRemoteUserUserTimelineCacheMax : 800 ,
2023-07-15 13:12:20 +02:00
blockedHosts : [ ] as string [ ] ,
sensitiveWords : [ ] as string [ ] ,
2024-02-13 04:54:01 +09:00
prohibitedWords : [ ] as string [ ] ,
2023-09-05 17:02:14 +09:00
} as MiMeta ;
2024-09-22 12:53:13 +09:00
const meta = { . . . metaInitial } ;
function updateMeta ( newMeta : Partial < MiMeta > ) : void {
for ( const key in meta ) {
delete ( meta as any ) [ key ] ;
}
Object . assign ( meta , newMeta ) ;
}
2023-07-15 13:12:20 +02:00
2023-07-09 01:59:44 +02:00
beforeAll ( async ( ) = > {
2023-02-19 07:27:14 +01:00
const app = await Test . createTestingModule ( {
imports : [ GlobalModule , CoreModule ] ,
2023-07-15 13:12:20 +02:00
} )
. overrideProvider ( DownloadService ) . useValue ( {
async downloadUrl ( ) : Promise < { filename : string } > {
return {
filename : 'dummy.tmp' ,
} ;
} ,
} )
2024-09-22 12:53:13 +09:00
. overrideProvider ( DI . meta ) . useFactory ( { factory : ( ) = > meta } )
. compile ( ) ;
2023-02-19 07:27:14 +01:00
await app . init ( ) ;
app . enableShutdownHooks ( ) ;
2024-08-09 12:10:51 +09:00
userProfilesRepository = app . get ( DI . userProfilesRepository ) ;
2023-02-19 07:27:14 +01:00
noteService = app . get < ApNoteService > ( ApNoteService ) ;
personService = app . get < ApPersonService > ( ApPersonService ) ;
2023-03-12 04:11:37 +01:00
rendererService = app . get < ApRendererService > ( ApRendererService ) ;
2023-07-15 13:12:20 +02:00
imageService = app . get < ApImageService > ( ApImageService ) ;
2024-05-01 07:33:58 +00:00
jsonLdService = app . get < JsonLdService > ( JsonLdService ) ;
2023-02-19 07:27:14 +01:00
resolver = new MockResolver ( await app . resolve < LoggerService > ( LoggerService ) ) ;
2025-02-12 14:13:00 -05:00
idService = app . get < IdService > ( IdService ) ;
2025-02-22 12:02:16 -05:00
userPublickeysRepository = app . get < UserPublickeysRepository > ( DI . userPublickeysRepository ) ;
2025-02-21 22:04:36 -05:00
userKeypairService = app . get < UserKeypairService > ( UserKeypairService ) ;
2025-02-22 20:33:45 -05:00
config = app . get < Config > ( DI . config ) ;
2023-02-19 07:27:14 +01:00
// Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error
const federatedInstanceService = app . get < FederatedInstanceService > ( FederatedInstanceService ) ;
2023-07-09 01:59:44 +02:00
jest . spyOn ( federatedInstanceService , 'fetch' ) . mockImplementation ( ( ) = > new Promise ( ( ) = > { } ) ) ;
} ) ;
beforeEach ( ( ) = > {
resolver . clear ( ) ;
2023-02-19 07:27:14 +01:00
} ) ;
2021-07-10 23:14:57 +09:00
describe ( 'Parse minimum object' , ( ) = > {
2023-02-19 09:49:18 +01:00
const actor = createRandomActor ( ) ;
2021-07-10 23:14:57 +09:00
const post = {
'@context' : 'https://www.w3.org/ns/activitystreams' ,
2023-06-25 04:04:33 +02:00
id : ` ${ host } /users/ ${ secureRndstr ( 8 ) } ` ,
2021-07-10 23:14:57 +09:00
type : 'Note' ,
attributedTo : actor.id ,
to : 'https://www.w3.org/ns/activitystreams#Public' ,
content : 'あ' ,
} ;
2023-02-02 18:18:25 +09:00
test ( 'Minimum Actor' , async ( ) = > {
2023-07-09 01:59:44 +02:00
resolver . register ( actor . id , actor ) ;
2021-07-10 23:14:57 +09:00
2023-02-19 07:27:14 +01:00
const user = await personService . createPerson ( actor . id , resolver ) ;
2021-07-10 23:14:57 +09:00
assert . deepStrictEqual ( user . uri , actor . id ) ;
assert . deepStrictEqual ( user . username , actor . preferredUsername ) ;
assert . deepStrictEqual ( user . inbox , actor . inbox ) ;
} ) ;
2023-02-02 18:18:25 +09:00
test ( 'Minimum Note' , async ( ) = > {
2023-07-09 01:59:44 +02:00
resolver . register ( actor . id , actor ) ;
resolver . register ( post . id , post ) ;
2021-07-10 23:14:57 +09:00
2024-11-20 20:16:43 -05:00
const note = await noteService . createNote ( post . id , undefined , resolver , true ) ;
2021-07-10 23:14:57 +09:00
assert . deepStrictEqual ( note ? . uri , post . id ) ;
2022-05-21 22:21:41 +09:00
assert . deepStrictEqual ( note . visibility , 'public' ) ;
assert . deepStrictEqual ( note . text , post . content ) ;
2021-07-10 23:14:57 +09:00
} ) ;
} ) ;
2021-08-17 17:25:19 +09:00
2023-02-19 09:49:18 +01:00
describe ( 'Name field' , ( ) = > {
test ( 'Truncate long name' , async ( ) = > {
const actor = {
. . . createRandomActor ( ) ,
2023-06-25 04:04:33 +02:00
name : secureRndstr ( 129 ) ,
2023-02-19 09:49:18 +01:00
} ;
2021-08-17 17:25:19 +09:00
2023-07-09 01:59:44 +02:00
resolver . register ( actor . id , actor ) ;
2021-08-17 17:25:19 +09:00
2023-02-19 09:49:18 +01:00
const user = await personService . createPerson ( actor . id , resolver ) ;
assert . deepStrictEqual ( user . name , actor . name . slice ( 0 , 128 ) ) ;
} ) ;
test ( 'Normalize empty name' , async ( ) = > {
const actor = {
. . . createRandomActor ( ) ,
name : '' ,
} ;
2021-08-17 17:25:19 +09:00
2023-07-09 01:59:44 +02:00
resolver . register ( actor . id , actor ) ;
2021-08-17 17:25:19 +09:00
2023-02-19 07:27:14 +01:00
const user = await personService . createPerson ( actor . id , resolver ) ;
2021-08-17 17:25:19 +09:00
2023-02-19 09:49:18 +01:00
assert . strictEqual ( user . name , null ) ;
2021-08-17 17:25:19 +09:00
} ) ;
} ) ;
2023-03-12 04:11:37 +01:00
2024-08-09 12:10:51 +09:00
describe ( 'Collection visibility' , ( ) = > {
test ( 'Public following/followers' , async ( ) = > {
const actor = createRandomActor ( ) ;
actor . following = {
id : ` ${ actor . id } /following ` ,
type : 'OrderedCollection' ,
totalItems : 0 ,
first : ` ${ actor . id } /following?page=1 ` ,
} ;
actor . followers = ` ${ actor . id } /followers ` ;
resolver . register ( actor . id , actor ) ;
resolver . register ( actor . followers , {
id : actor.followers ,
type : 'OrderedCollection' ,
totalItems : 0 ,
first : ` ${ actor . followers } ?page=1 ` ,
} ) ;
const user = await personService . createPerson ( actor . id , resolver ) ;
const userProfile = await userProfilesRepository . findOneByOrFail ( { userId : user.id } ) ;
assert . deepStrictEqual ( userProfile . followingVisibility , 'public' ) ;
assert . deepStrictEqual ( userProfile . followersVisibility , 'public' ) ;
} ) ;
test ( 'Private following/followers' , async ( ) = > {
const actor = createRandomActor ( ) ;
actor . following = {
id : ` ${ actor . id } /following ` ,
type : 'OrderedCollection' ,
totalItems : 0 ,
// first: …
} ;
actor . followers = ` ${ actor . id } /followers ` ;
resolver . register ( actor . id , actor ) ;
//resolver.register(actor.followers, { … });
const user = await personService . createPerson ( actor . id , resolver ) ;
const userProfile = await userProfilesRepository . findOneByOrFail ( { userId : user.id } ) ;
assert . deepStrictEqual ( userProfile . followingVisibility , 'private' ) ;
assert . deepStrictEqual ( userProfile . followersVisibility , 'private' ) ;
} ) ;
} ) ;
2023-03-12 04:11:37 +01:00
describe ( 'Renderer' , ( ) = > {
test ( 'Render an announce with visibility: followers' , ( ) = > {
2024-02-17 12:41:19 +09:00
rendererService . renderAnnounce ( 'https://example.com/notes/00example' , {
2023-10-16 12:58:17 +09:00
id : genAidx ( Date . now ( ) ) ,
2023-03-12 04:11:37 +01:00
visibility : 'followers' ,
2023-09-05 17:02:14 +09:00
} as MiNote ) ;
2023-03-12 04:11:37 +01:00
} ) ;
} ) ;
2023-07-09 01:59:44 +02:00
describe ( 'Featured' , ( ) = > {
test ( 'Fetch featured notes from IActor' , async ( ) = > {
const actor = createRandomActor ( ) ;
actor . featured = ` ${ actor . id } /collections/featured ` ;
const featured = createRandomFeaturedCollection ( actor , 5 ) ;
resolver . register ( actor . id , actor ) ;
resolver . register ( actor . featured , featured ) ;
await personService . createPerson ( actor . id , resolver ) ;
// All notes in `featured` are same-origin, no need to fetch notes again
2024-03-02 16:36:49 +00:00
assert . deepStrictEqual ( resolver . remoteGetTrials ( ) , [ actor . id , ` ${ actor . id } /outbox ` , actor . featured ] ) ;
2023-07-09 01:59:44 +02:00
// Created notes without resolving anything
for ( const item of featured . items as IPost [ ] ) {
const note = await noteService . fetchNote ( item ) ;
assert . ok ( note ) ;
assert . strictEqual ( note . text , 'test test foo' ) ;
assert . strictEqual ( note . uri , item . id ) ;
}
} ) ;
test ( 'Fetch featured notes from IActor pointing to another remote server' , async ( ) = > {
const actor1 = createRandomActor ( ) ;
actor1 . featured = ` ${ actor1 . id } /collections/featured ` ;
const actor2 = createRandomActor ( { actorHost : 'https://host2.test' } ) ;
const actor2Note = createRandomNote ( actor2 ) ;
const featured = createRandomFeaturedCollection ( actor1 , 0 ) ;
( featured . items as IPost [ ] ) . push ( {
. . . actor2Note ,
content : 'test test bar' , // fraud!
} ) ;
resolver . register ( actor1 . id , actor1 ) ;
resolver . register ( actor1 . featured , featured ) ;
resolver . register ( actor2 . id , actor2 ) ;
resolver . register ( actor2Note . id , actor2Note ) ;
await personService . createPerson ( actor1 . id , resolver ) ;
// actor2Note is from a different server and needs to be fetched again
assert . deepStrictEqual (
resolver . remoteGetTrials ( ) ,
2024-10-15 21:32:26 -04:00
[ actor1 . id , ` ${ actor1 . id } /outbox ` , actor1 . featured , actor2Note . id , actor2 . id , ` ${ actor2 . id } /outbox ` ] ,
2023-07-09 01:59:44 +02:00
) ;
const note = await noteService . fetchNote ( actor2Note . id ) ;
assert . ok ( note ) ;
// Reflects the original content instead of the fraud
assert . strictEqual ( note . text , 'test test foo' ) ;
assert . strictEqual ( note . uri , actor2Note . id ) ;
} ) ;
2023-08-08 06:26:03 +02:00
test ( 'Fetch a note that is a featured note of the attributed actor' , async ( ) = > {
const actor = createRandomActor ( ) ;
actor . featured = ` ${ actor . id } /collections/featured ` ;
const featured = createRandomFeaturedCollection ( actor , 5 ) ;
const firstNote = ( featured . items as NonTransientIPost [ ] ) [ 0 ] ;
resolver . register ( actor . id , actor ) ;
resolver . register ( actor . featured , featured ) ;
resolver . register ( firstNote . id , firstNote ) ;
2024-11-20 20:16:43 -05:00
const note = await noteService . createNote ( firstNote . id as string , undefined , resolver ) ;
2023-08-08 06:26:03 +02:00
assert . strictEqual ( note ? . uri , firstNote . id ) ;
} ) ;
2023-07-09 01:59:44 +02:00
} ) ;
2023-07-15 13:12:20 +02:00
describe ( 'Images' , ( ) = > {
test ( 'Create images' , async ( ) = > {
const imageObject : IApDocument = {
type : 'Document' ,
mediaType : 'image/png' ,
url : 'http://host1.test/foo.png' ,
name : '' ,
} ;
const driveFile = await imageService . createImage (
await createRandomRemoteUser ( resolver , personService ) ,
imageObject ,
) ;
2024-04-28 10:53:33 +09:00
assert . ok ( driveFile && ! driveFile . isLink ) ;
2023-07-15 13:12:20 +02:00
const sensitiveImageObject : IApDocument = {
type : 'Document' ,
mediaType : 'image/png' ,
url : 'http://host1.test/bar.png' ,
name : '' ,
sensitive : true ,
} ;
const sensitiveDriveFile = await imageService . createImage (
await createRandomRemoteUser ( resolver , personService ) ,
sensitiveImageObject ,
) ;
2024-04-28 10:53:33 +09:00
assert . ok ( sensitiveDriveFile && ! sensitiveDriveFile . isLink ) ;
2023-07-15 13:12:20 +02:00
} ) ;
test ( 'cacheRemoteFiles=false disables caching' , async ( ) = > {
2024-09-22 12:53:13 +09:00
updateMeta ( { . . . metaInitial , cacheRemoteFiles : false } ) ;
2023-07-15 13:12:20 +02:00
const imageObject : IApDocument = {
type : 'Document' ,
mediaType : 'image/png' ,
url : 'http://host1.test/foo.png' ,
name : '' ,
} ;
const driveFile = await imageService . createImage (
await createRandomRemoteUser ( resolver , personService ) ,
imageObject ,
) ;
2024-04-28 10:53:33 +09:00
assert . ok ( driveFile && driveFile . isLink ) ;
2023-07-15 13:12:20 +02:00
const sensitiveImageObject : IApDocument = {
type : 'Document' ,
mediaType : 'image/png' ,
url : 'http://host1.test/bar.png' ,
name : '' ,
sensitive : true ,
} ;
const sensitiveDriveFile = await imageService . createImage (
await createRandomRemoteUser ( resolver , personService ) ,
sensitiveImageObject ,
) ;
2024-04-28 10:53:33 +09:00
assert . ok ( sensitiveDriveFile && sensitiveDriveFile . isLink ) ;
2023-07-15 13:12:20 +02:00
} ) ;
test ( 'cacheRemoteSensitiveFiles=false only affects sensitive files' , async ( ) = > {
2024-09-22 12:53:13 +09:00
updateMeta ( { . . . metaInitial , cacheRemoteSensitiveFiles : false } ) ;
2023-07-15 13:12:20 +02:00
const imageObject : IApDocument = {
type : 'Document' ,
mediaType : 'image/png' ,
url : 'http://host1.test/foo.png' ,
name : '' ,
} ;
const driveFile = await imageService . createImage (
await createRandomRemoteUser ( resolver , personService ) ,
imageObject ,
) ;
2024-04-28 10:53:33 +09:00
assert . ok ( driveFile && ! driveFile . isLink ) ;
2023-07-15 13:12:20 +02:00
const sensitiveImageObject : IApDocument = {
type : 'Document' ,
mediaType : 'image/png' ,
url : 'http://host1.test/bar.png' ,
name : '' ,
sensitive : true ,
} ;
const sensitiveDriveFile = await imageService . createImage (
await createRandomRemoteUser ( resolver , personService ) ,
sensitiveImageObject ,
) ;
2024-04-28 10:53:33 +09:00
assert . ok ( sensitiveDriveFile && sensitiveDriveFile . isLink ) ;
} ) ;
test ( 'Link is not an attachment files' , async ( ) = > {
const linkObject : IObject = {
type : 'Link' ,
href : 'https://example.com/' ,
} ;
const driveFile = await imageService . createImage (
await createRandomRemoteUser ( resolver , personService ) ,
linkObject ,
) ;
assert . strictEqual ( driveFile , null ) ;
2023-07-15 13:12:20 +02:00
} ) ;
} ) ;
2024-05-01 07:33:58 +00:00
2024-10-15 21:32:26 -04:00
describe ( 'JSON-LD' , ( ) = > {
2024-05-01 07:33:58 +00:00
test ( 'Compaction' , async ( ) = > {
const jsonLd = jsonLdService . use ( ) ;
const object = {
'@context' : [
'https://www.w3.org/ns/activitystreams' ,
{
_misskey_quote : 'https://misskey-hub.net/ns#_misskey_quote' ,
unknown : 'https://example.org/ns#unknown' ,
undefined : null ,
} ,
] ,
id : 'https://example.com/notes/42' ,
type : 'Note' ,
attributedTo : 'https://example.com/users/1' ,
to : [ 'https://www.w3.org/ns/activitystreams#Public' ] ,
content : 'test test foo' ,
_misskey_quote : 'https://example.com/notes/1' ,
unknown : 'test test bar' ,
undefined : 'test test baz' ,
} ;
const compacted = await jsonLd . compact ( object ) ;
assert . deepStrictEqual ( compacted , {
'@context' : CONTEXT ,
id : 'https://example.com/notes/42' ,
type : 'Note' ,
attributedTo : 'https://example.com/users/1' ,
to : 'as:Public' ,
content : 'test test foo' ,
_misskey_quote : 'https://example.com/notes/1' ,
'https://example.org/ns#unknown' : 'test test bar' ,
// undefined: 'test test baz',
} ) ;
2023-07-15 13:12:20 +02:00
} ) ;
} ) ;
2025-02-12 14:13:00 -05:00
describe ( ApRendererService , ( ) = > {
2025-02-12 15:11:19 -05:00
let note : MiNote ;
2025-02-21 22:04:36 -05:00
let author : MiLocalUser ;
let keypair : MiUserKeypair ;
2025-02-12 14:13:00 -05:00
2025-02-21 22:04:36 -05:00
beforeEach ( async ( ) = > {
2025-02-12 15:11:19 -05:00
author = new MiUser ( {
id : idService.gen ( ) ,
2025-02-21 22:04:36 -05:00
host : null ,
uri : null ,
username : 'testAuthor' ,
usernameLower : 'testauthor' ,
name : 'Test Author' ,
isCat : true ,
requireSigninToViewContents : true ,
makeNotesFollowersOnlyBefore : new Date ( 2025 , 2 , 20 ) . valueOf ( ) ,
makeNotesHiddenBefore : new Date ( 2025 , 2 , 21 ) . valueOf ( ) ,
isLocked : true ,
isExplorable : true ,
hideOnlineStatus : true ,
noindex : true ,
enableRss : true ,
} ) as MiLocalUser ;
const [ publicKey , privateKey ] = await new Promise < [ string , string ] > ( ( res , rej ) = >
generateKeyPair ( 'rsa' , {
modulusLength : 2048 ,
publicKeyEncoding : {
type : 'spki' ,
format : 'pem' ,
} ,
privateKeyEncoding : {
type : 'pkcs8' ,
format : 'pem' ,
cipher : undefined ,
passphrase : undefined ,
} ,
} , ( err , publicKey , privateKey ) = >
err ? rej ( err ) : res ( [ publicKey , privateKey ] ) ,
) ) ;
keypair = new MiUserKeypair ( {
userId : author.id ,
user : author ,
publicKey ,
privateKey ,
2025-02-12 15:11:19 -05:00
} ) ;
2025-02-21 22:04:36 -05:00
( ( userKeypairService as unknown as { cache : RedisKVCache < MiUserKeypair > } ) . cache as unknown as { memoryCache : MemoryKVCache < MiUserKeypair > } ) . memoryCache . set ( author . id , keypair ) ;
2025-02-12 15:11:19 -05:00
note = new MiNote ( {
id : idService.gen ( ) ,
userId : author.id ,
2025-02-21 22:04:36 -05:00
user : author ,
2025-02-12 15:11:19 -05:00
visibility : 'public' ,
localOnly : false ,
text : 'Note text' ,
cw : null ,
renoteCount : 0 ,
repliesCount : 0 ,
clippedCount : 0 ,
reactions : { } ,
fileIds : [ ] ,
attachedFileTypes : [ ] ,
visibleUserIds : [ ] ,
mentions : [ ] ,
// This is fucked tbh - it's JSON stored in a TEXT column that gets parsed/serialized all over the place
mentionedRemoteUsers : '[]' ,
reactionAndUserPairCache : [ ] ,
emojis : [ ] ,
tags : [ ] ,
hasPoll : false ,
2025-02-12 14:13:00 -05:00
} ) ;
2025-02-12 15:11:19 -05:00
} ) ;
2025-02-12 14:13:00 -05:00
2025-02-12 15:11:19 -05:00
describe ( 'renderNote' , ( ) = > {
2025-02-12 14:13:00 -05:00
describe ( 'summary' , ( ) = > {
// I actually don't know why it does this, but the logic was already there so I've preserved it.
2025-02-15 11:40:18 -05:00
it ( 'should be zero-width space when CW is empty string' , async ( ) = > {
2025-02-12 14:13:00 -05:00
note . cw = '' ;
const result = await rendererService . renderNote ( note , author , false ) ;
expect ( result . summary ) . toBe ( String . fromCharCode ( 0x200B ) ) ;
} ) ;
it ( 'should be undefined when CW is null' , async ( ) = > {
const result = await rendererService . renderNote ( note , author , false ) ;
expect ( result . summary ) . toBeUndefined ( ) ;
} ) ;
it ( 'should be CW when present without mandatoryCW' , async ( ) = > {
note . cw = 'original' ;
const result = await rendererService . renderNote ( note , author , false ) ;
expect ( result . summary ) . toBe ( 'original' ) ;
} ) ;
it ( 'should be mandatoryCW when present without CW' , async ( ) = > {
author . mandatoryCW = 'mandatory' ;
const result = await rendererService . renderNote ( note , author , false ) ;
expect ( result . summary ) . toBe ( 'mandatory' ) ;
} ) ;
it ( 'should be merged when CW and mandatoryCW are both present' , async ( ) = > {
note . cw = 'original' ;
author . mandatoryCW = 'mandatory' ;
const result = await rendererService . renderNote ( note , author , false ) ;
expect ( result . summary ) . toBe ( 'original, mandatory' ) ;
} ) ;
it ( 'should be CW when CW includes mandatoryCW' , async ( ) = > {
note . cw = 'original and mandatory' ;
author . mandatoryCW = 'mandatory' ;
const result = await rendererService . renderNote ( note , author , false ) ;
expect ( result . summary ) . toBe ( 'original and mandatory' ) ;
} ) ;
} ) ;
2025-02-22 20:33:45 -05:00
describe ( 'replies' , ( ) = > {
it ( 'should be included when visibility=public' , async ( ) = > {
note . visibility = 'public' ;
const rendered = await rendererService . renderNote ( note , author , false ) ;
expect ( rendered . replies ) . toBeDefined ( ) ;
} ) ;
it ( 'should be included when visibility=home' , async ( ) = > {
note . visibility = 'home' ;
const rendered = await rendererService . renderNote ( note , author , false ) ;
expect ( rendered . replies ) . toBeDefined ( ) ;
} ) ;
it ( 'should be excluded when visibility=followers' , async ( ) = > {
note . visibility = 'followers' ;
const rendered = await rendererService . renderNote ( note , author , false ) ;
expect ( rendered . replies ) . not . toBeDefined ( ) ;
} ) ;
it ( 'should be excluded when visibility=specified' , async ( ) = > {
note . visibility = 'specified' ;
const rendered = await rendererService . renderNote ( note , author , false ) ;
expect ( rendered . replies ) . not . toBeDefined ( ) ;
} ) ;
} ) ;
2025-02-12 14:13:00 -05:00
} ) ;
2025-02-12 15:11:19 -05:00
describe ( 'renderUpnote' , ( ) = > {
describe ( 'summary' , ( ) = > {
// I actually don't know why it does this, but the logic was already there so I've preserved it.
2025-02-15 11:40:18 -05:00
it ( 'should be zero-width space when CW is empty string' , async ( ) = > {
2025-02-12 15:11:19 -05:00
note . cw = '' ;
const result = await rendererService . renderUpNote ( note , author , false ) ;
expect ( result . summary ) . toBe ( String . fromCharCode ( 0x200B ) ) ;
} ) ;
it ( 'should be undefined when CW is null' , async ( ) = > {
const result = await rendererService . renderUpNote ( note , author , false ) ;
expect ( result . summary ) . toBeUndefined ( ) ;
} ) ;
it ( 'should be CW when present without mandatoryCW' , async ( ) = > {
note . cw = 'original' ;
const result = await rendererService . renderUpNote ( note , author , false ) ;
expect ( result . summary ) . toBe ( 'original' ) ;
} ) ;
it ( 'should be mandatoryCW when present without CW' , async ( ) = > {
author . mandatoryCW = 'mandatory' ;
const result = await rendererService . renderUpNote ( note , author , false ) ;
expect ( result . summary ) . toBe ( 'mandatory' ) ;
} ) ;
it ( 'should be merged when CW and mandatoryCW are both present' , async ( ) = > {
note . cw = 'original' ;
author . mandatoryCW = 'mandatory' ;
const result = await rendererService . renderUpNote ( note , author , false ) ;
expect ( result . summary ) . toBe ( 'original, mandatory' ) ;
} ) ;
it ( 'should be CW when CW includes mandatoryCW' , async ( ) = > {
note . cw = 'original and mandatory' ;
author . mandatoryCW = 'mandatory' ;
const result = await rendererService . renderUpNote ( note , author , false ) ;
expect ( result . summary ) . toBe ( 'original and mandatory' ) ;
} ) ;
} ) ;
} ) ;
2025-02-21 22:04:36 -05:00
describe ( 'renderPersonRedacted' , ( ) = > {
it ( 'should include minimal properties' , async ( ) = > {
const result = await rendererService . renderPersonRedacted ( author ) ;
expect ( result . type ) . toBe ( 'Person' ) ;
expect ( result . id ) . toBeTruthy ( ) ;
expect ( result . inbox ) . toBeTruthy ( ) ;
expect ( result . sharedInbox ) . toBeTruthy ( ) ;
expect ( result . endpoints . sharedInbox ) . toBeTruthy ( ) ;
expect ( result . url ) . toBeTruthy ( ) ;
expect ( result . preferredUsername ) . toBe ( author . username ) ;
expect ( result . publicKey . owner ) . toBe ( result . id ) ;
expect ( result . _misskey_requireSigninToViewContents ) . toBe ( author . requireSigninToViewContents ) ;
expect ( result . _misskey_makeNotesFollowersOnlyBefore ) . toBe ( author . makeNotesFollowersOnlyBefore ) ;
expect ( result . _misskey_makeNotesHiddenBefore ) . toBe ( author . makeNotesHiddenBefore ) ;
expect ( result . discoverable ) . toBe ( author . isExplorable ) ;
expect ( result . hideOnlineStatus ) . toBe ( author . hideOnlineStatus ) ;
expect ( result . noindex ) . toBe ( author . noindex ) ;
expect ( result . indexable ) . toBe ( ! author . noindex ) ;
expect ( result . enableRss ) . toBe ( author . enableRss ) ;
} ) ;
it ( 'should not include sensitive properties' , async ( ) = > {
const result = await rendererService . renderPersonRedacted ( author ) as IActor ;
expect ( result . name ) . toBeUndefined ( ) ;
} ) ;
} ) ;
2025-02-22 20:33:45 -05:00
describe ( 'renderRepliesCollection' , ( ) = > {
it ( 'should include type' , async ( ) = > {
const collection = await rendererService . renderRepliesCollection ( note . id ) ;
expect ( collection . type ) . toBe ( 'OrderedCollection' ) ;
} ) ;
it ( 'should include id' , async ( ) = > {
const collection = await rendererService . renderRepliesCollection ( note . id ) ;
expect ( collection . id ) . toBe ( ` ${ config . url } /notes/ ${ note . id } /replies ` ) ;
} ) ;
it ( 'should include first' , async ( ) = > {
const collection = await rendererService . renderRepliesCollection ( note . id ) ;
expect ( collection . first ) . toBe ( ` ${ config . url } /notes/ ${ note . id } /replies?page=true ` ) ;
} ) ;
it ( 'should include totalItems' , async ( ) = > {
const collection = await rendererService . renderRepliesCollection ( note . id ) ;
expect ( collection . totalItems ) . toBe ( 0 ) ;
} ) ;
} ) ;
describe ( 'renderRepliesCollectionPage' , ( ) = > {
describe ( 'with untilId' , ( ) = > {
it ( 'should include type' , async ( ) = > {
const collection = await rendererService . renderRepliesCollectionPage ( note . id , 'abc123' ) ;
expect ( collection . type ) . toBe ( 'OrderedCollectionPage' ) ;
} ) ;
it ( 'should include id' , async ( ) = > {
const collection = await rendererService . renderRepliesCollectionPage ( note . id , 'abc123' ) ;
expect ( collection . id ) . toBe ( ` ${ config . url } /notes/ ${ note . id } /replies?page=true&until_id=abc123 ` ) ;
} ) ;
it ( 'should include partOf' , async ( ) = > {
const collection = await rendererService . renderRepliesCollectionPage ( note . id , 'abc123' ) ;
expect ( collection . partOf ) . toBe ( ` ${ config . url } /notes/ ${ note . id } /replies ` ) ;
} ) ;
it ( 'should include first' , async ( ) = > {
const collection = await rendererService . renderRepliesCollectionPage ( note . id , 'abc123' ) ;
expect ( collection . first ) . toBe ( ` ${ config . url } /notes/ ${ note . id } /replies?page=true ` ) ;
} ) ;
it ( 'should include totalItems' , async ( ) = > {
const collection = await rendererService . renderRepliesCollectionPage ( note . id , 'abc123' ) ;
expect ( collection . totalItems ) . toBe ( 0 ) ;
} ) ;
it ( 'should include orderedItems' , async ( ) = > {
const collection = await rendererService . renderRepliesCollectionPage ( note . id , 'abc123' ) ;
expect ( collection . orderedItems ) . toBeDefined ( ) ;
} ) ;
} ) ;
describe ( 'without untilId' , ( ) = > {
it ( 'should include type' , async ( ) = > {
const collection = await rendererService . renderRepliesCollectionPage ( note . id , undefined ) ;
expect ( collection . type ) . toBe ( 'OrderedCollectionPage' ) ;
} ) ;
it ( 'should include id' , async ( ) = > {
const collection = await rendererService . renderRepliesCollectionPage ( note . id , undefined ) ;
expect ( collection . id ) . toBe ( ` ${ config . url } /notes/ ${ note . id } /replies?page=true ` ) ;
} ) ;
it ( 'should include partOf' , async ( ) = > {
const collection = await rendererService . renderRepliesCollectionPage ( note . id , undefined ) ;
expect ( collection . partOf ) . toBe ( ` ${ config . url } /notes/ ${ note . id } /replies ` ) ;
} ) ;
it ( 'should include first' , async ( ) = > {
const collection = await rendererService . renderRepliesCollectionPage ( note . id , undefined ) ;
expect ( collection . first ) . toBe ( ` ${ config . url } /notes/ ${ note . id } /replies?page=true ` ) ;
} ) ;
it ( 'should include totalItems' , async ( ) = > {
const collection = await rendererService . renderRepliesCollectionPage ( note . id , undefined ) ;
expect ( collection . totalItems ) . toBe ( 0 ) ;
} ) ;
it ( 'should include orderedItems' , async ( ) = > {
const collection = await rendererService . renderRepliesCollectionPage ( note . id , undefined ) ;
expect ( collection . orderedItems ) . toBeDefined ( ) ;
} ) ;
} ) ;
} ) ;
2025-02-12 14:13:00 -05:00
} ) ;
2025-02-22 12:02:16 -05:00
describe ( ApPersonService , ( ) = > {
describe ( 'createPerson' , ( ) = > {
it ( 'should trim publicKey' , async ( ) = > {
const actor = createRandomActor ( ) ;
actor . publicKey = {
id : ` ${ actor . id } #main-key ` ,
publicKeyPem : ' key material\t\n\r\n \n' ,
} ;
resolver . register ( actor . id , actor ) ;
const user = await personService . createPerson ( actor . id , resolver ) ;
const publicKey = await userPublickeysRepository . findOneBy ( { userId : user.id } ) ;
expect ( publicKey ) . not . toBeNull ( ) ;
expect ( publicKey ? . keyPem ) . toBe ( 'key material' ) ;
} ) ;
it ( 'should accept SocialHome actor' , async ( ) = > {
2025-02-25 11:44:16 -05:00
// This is taken from a real SocialHome actor, including the 13,905 newline characters in the public key.
2025-02-22 12:02:16 -05:00
const actor = {
'@context' : [ 'https://www.w3.org/ns/activitystreams' , 'https://w3id.org/security/v1' , {
'pyfed' : 'https://docs.jasonrobinson.me/ns/python-federation#' ,
'diaspora' : 'https://diasporafoundation.org/ns/' ,
'manuallyApprovesFollowers' : 'as:manuallyApprovesFollowers' ,
} ] ,
id : 'https://socialhome.network/u/hq/' ,
type : 'Person' ,
inbox : 'https://socialhome.network/u/hq/inbox/' ,
'diaspora:guid' : '7538bd1b-d3a8-49a5-bf00-db63fcc9114f' ,
'diaspora:handle' : 'hq@socialhome.network' ,
publicKey : {
id : 'https://socialhome.network/u/hq/#main-key' ,
owner : 'https://socialhome.network/u/hq/' ,
2025-02-25 11:44:16 -05:00
publicKeyPem : '-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAg39sDmTAJ7l9bl5jYLmj\nKYnDZJgRiO/WR+V1HEMEsRoEPTxJzWe+Ou7YTUhOOvDRu5ncEn3ictF3/BxhhQC1\nQwUKYlfuU1R7PyGqWtGm6300mDAmbq+eyC+fwV9FbkCm9npRatZfnZXZWuCgA6f7\nWmmBw09QVZQ6Ypu+7CF/Q6bv0E5B2hieTSbRgavdSkEopMyJhPs5/X6Hh4XYSi7t\nYEg9vD0d0J9QJSnCTYIZT145cV1DANV/4KjhKkYgvt4hLNOKZ1v4QC57K+PFna9N\ntxm1nMxwjpBPus8LQeDii/MwKoiZ7LBjeflm0C9AMFlNPB9iq3rEXo3eyCEb7Lyr\nEp+oqYNfopFIRPNfhBxtkx5ioUXty3cx1WnZtehqGdpOcb1wUatW5IjV8tlfLIr7\nrDNCxgGnScR6h7++BHYDdDVBgGUkC5ELIxxSMqlYMiBGVmYdIoAGO6nuqw4bp5l3\nUf07d28GoZgcRBVZWC/xOtRb7E6PTzsE7xd51UijusRC79lnapzTWY9GAY0ZYu+w\nbAxO7u3+Knr6EXZkGkmrElKIT2N6SPJY3Xo91+PT1Y77JMFkkWlEX9IO08fALsqg\nbMSKNQ8WfyHCTjaiH3n4BdgTjP4kRm2OhczxvgCFvtcOK+M60YdwM6MOZDEOVtGU\nGIYA1mtQW7a8jb5QPTQu9GcCAwEAAQ==\n-----END PUBLIC KEY-----' + '' . padEnd ( 13905 , '\n' ) ,
2025-02-22 12:02:16 -05:00
} ,
endpoints : { 'sharedInbox' : 'https://socialhome.network/receive/public/' } ,
followers : 'https://socialhome.network/u/hq/followers/' ,
following : 'https://socialhome.network/u/hq/following/' ,
icon : {
type : 'Image' ,
'pyfed:inlineImage' : false ,
mediaType : 'image/png' ,
url : 'https://socialhome.network/media/__sized__/profiles/Socialhome-dark-600-crop-c0-5__0-5-300x300.png' ,
} ,
manuallyApprovesFollowers : false ,
name : 'Socialhome HQ' ,
outbox : 'https://socialhome.network/u/hq/outbox/' ,
preferredUsername : 'hq' ,
published : '2017-01-29T19:28:19+00:00' ,
updated : '2025-02-17T23:11:30+00:00' ,
url : 'https://socialhome.network/p/7538bd1b-d3a8-49a5-bf00-db63fcc9114f/' ,
} ;
resolver . register ( actor . id , actor ) ;
resolver . register ( actor . publicKey . id , actor . publicKey ) ;
resolver . register ( actor . followers , { id : actor.followers , type : 'Collection' , totalItems : 0 , items : [ ] } satisfies ICollection ) ;
resolver . register ( actor . following , { id : actor.following , type : 'Collection' , totalItems : 0 , items : [ ] } satisfies ICollection ) ;
resolver . register ( actor . outbox , { id : actor.outbox , type : 'Collection' , totalItems : 0 , items : [ ] } satisfies ICollection ) ;
const user = await personService . createPerson ( actor . id , resolver ) ;
const publicKey = await userPublickeysRepository . findOneBy ( { userId : user.id } ) ;
expect ( user . uri ) . toBe ( actor . id ) ;
expect ( publicKey ) . not . toBeNull ( ) ;
} ) ;
} ) ;
} ) ;
2021-07-10 23:14:57 +09:00
} ) ;