aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/tests/api
diff options
context:
space:
mode:
Diffstat (limited to 'server/tests/api')
-rw-r--r--server/tests/api/activitypub/client.ts43
-rw-r--r--server/tests/api/activitypub/fetch.ts86
-rw-r--r--server/tests/api/activitypub/helpers.ts182
-rw-r--r--server/tests/api/activitypub/index.ts4
-rw-r--r--server/tests/api/activitypub/json/mastodon/bad-body-http-signature.json93
-rw-r--r--server/tests/api/activitypub/json/mastodon/bad-http-signature.json93
-rw-r--r--server/tests/api/activitypub/json/mastodon/bad-public-key.json3
-rw-r--r--server/tests/api/activitypub/json/mastodon/create-bad-signature.json81
-rw-r--r--server/tests/api/activitypub/json/mastodon/create.json81
-rw-r--r--server/tests/api/activitypub/json/mastodon/http-signature.json93
-rw-r--r--server/tests/api/activitypub/json/mastodon/public-key.json3
-rw-r--r--server/tests/api/activitypub/json/peertube/announce-without-context.json13
-rw-r--r--server/tests/api/activitypub/json/peertube/invalid-keys.json6
-rw-r--r--server/tests/api/activitypub/json/peertube/keys.json4
-rw-r--r--server/tests/api/activitypub/security.ts180
-rw-r--r--server/tests/api/index-4.ts1
-rw-r--r--server/tests/api/redundancy/redundancy.ts36
-rw-r--r--server/tests/api/server/handle-down.ts8
-rw-r--r--server/tests/api/server/index.ts1
-rw-r--r--server/tests/api/server/no-client.ts36
20 files changed, 1024 insertions, 23 deletions
diff --git a/server/tests/api/activitypub/client.ts b/server/tests/api/activitypub/client.ts
new file mode 100644
index 000000000..334cd4e5c
--- /dev/null
+++ b/server/tests/api/activitypub/client.ts
@@ -0,0 +1,43 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 flushTests,
7 killallServers,
8 makeActivityPubGetRequest,
9 runServer,
10 ServerInfo,
11 setAccessTokensToServers
12} from '../../shared/utils'
13
14
15const expect = chai.expect
16
17describe('Test activitypub', function () {
18 let server: ServerInfo = null
19
20 before(async function () {
21 this.timeout(30000)
22
23 await flushTests()
24
25 server = await runServer(1)
26
27 await setAccessTokensToServers([ server ])
28 })
29
30 it('Should return the account object', async function () {
31 const res = await makeActivityPubGetRequest(server.url, '/accounts/root')
32 const object = res.body
33
34 expect(object.type).to.equal('Person')
35 expect(object.id).to.equal('http://localhost:9001/accounts/root')
36 expect(object.name).to.equal('root')
37 expect(object.preferredUsername).to.equal('root')
38 })
39
40 after(async function () {
41 killallServers([ server ])
42 })
43})
diff --git a/server/tests/api/activitypub/fetch.ts b/server/tests/api/activitypub/fetch.ts
new file mode 100644
index 000000000..a42c606c6
--- /dev/null
+++ b/server/tests/api/activitypub/fetch.ts
@@ -0,0 +1,86 @@
1/* tslint:disable:no-unused-expression */
2
3import 'mocha'
4
5import {
6 createUser,
7 doubleFollow,
8 flushAndRunMultipleServers,
9 flushTests,
10 getVideosListSort,
11 killallServers,
12 ServerInfo,
13 setAccessTokensToServers,
14 uploadVideo,
15 userLogin
16} from '../../utils'
17import * as chai from 'chai'
18import { setActorField, setVideoField } from '../../utils/miscs/sql'
19import { waitJobs } from '../../utils/server/jobs'
20import { Video } from '../../../../shared/models/videos'
21
22const expect = chai.expect
23
24describe('Test ActivityPub fetcher', function () {
25 let servers: ServerInfo[]
26
27 // ---------------------------------------------------------------
28
29 before(async function () {
30 this.timeout(60000)
31
32 servers = await flushAndRunMultipleServers(3)
33
34 // Get the access tokens
35 await setAccessTokensToServers(servers)
36
37 const user = { username: 'user1', password: 'password' }
38 for (const server of servers) {
39 await createUser(server.url, server.accessToken, user.username, user.password)
40 }
41
42 const userAccessToken = await userLogin(servers[0], user)
43
44 await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video root' })
45 const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'bad video root' })
46 const badVideoUUID = res.body.video.uuid
47 await uploadVideo(servers[0].url, userAccessToken, { name: 'video user' })
48
49 await setActorField(1, 'http://localhost:9001/accounts/user1', 'url', 'http://localhost:9002/accounts/user1')
50 await setVideoField(1, badVideoUUID, 'url', 'http://localhost:9003/videos/watch/' + badVideoUUID)
51 })
52
53 it('Should add only the video with a valid actor URL', async function () {
54 this.timeout(60000)
55
56 await doubleFollow(servers[0], servers[1])
57 await waitJobs(servers)
58
59 {
60 const res = await getVideosListSort(servers[0].url, 'createdAt')
61 expect(res.body.total).to.equal(3)
62
63 const data: Video[] = res.body.data
64 expect(data[0].name).to.equal('video root')
65 expect(data[1].name).to.equal('bad video root')
66 expect(data[2].name).to.equal('video user')
67 }
68
69 {
70 const res = await getVideosListSort(servers[1].url, 'createdAt')
71 expect(res.body.total).to.equal(1)
72
73 const data: Video[] = res.body.data
74 expect(data[0].name).to.equal('video root')
75 }
76 })
77
78 after(async function () {
79 killallServers(servers)
80
81 // Keep the logs if the test failed
82 if (this['ok']) {
83 await flushTests()
84 }
85 })
86})
diff --git a/server/tests/api/activitypub/helpers.ts b/server/tests/api/activitypub/helpers.ts
new file mode 100644
index 000000000..610846247
--- /dev/null
+++ b/server/tests/api/activitypub/helpers.ts
@@ -0,0 +1,182 @@
1/* tslint:disable:no-unused-expression */
2
3import 'mocha'
4import { expect } from 'chai'
5import { buildRequestStub } from '../../utils'
6import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../../../helpers/peertube-crypto'
7import { cloneDeep } from 'lodash'
8import { buildSignedActivity } from '../../../helpers/activitypub'
9
10describe('Test activity pub helpers', function () {
11 describe('When checking the Linked Signature', function () {
12
13 it('Should fail with an invalid Mastodon signature', async function () {
14 const body = require('./json/mastodon/create-bad-signature.json')
15 const publicKey = require('./json/mastodon/public-key.json').publicKey
16 const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
17
18 const result = await isJsonLDSignatureVerified(fromActor as any, body)
19
20 expect(result).to.be.false
21 })
22
23 it('Should fail with an invalid public key', async function () {
24 const body = require('./json/mastodon/create.json')
25 const publicKey = require('./json/mastodon/bad-public-key.json').publicKey
26 const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
27
28 const result = await isJsonLDSignatureVerified(fromActor as any, body)
29
30 expect(result).to.be.false
31 })
32
33 it('Should succeed with a valid Mastodon signature', async function () {
34 const body = require('./json/mastodon/create.json')
35 const publicKey = require('./json/mastodon/public-key.json').publicKey
36 const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
37
38 const result = await isJsonLDSignatureVerified(fromActor as any, body)
39
40 expect(result).to.be.true
41 })
42
43 it('Should fail with an invalid PeerTube signature', async function () {
44 const keys = require('./json/peertube/invalid-keys.json')
45 const body = require('./json/peertube/announce-without-context.json')
46
47 const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
48 const signedBody = await buildSignedActivity(actorSignature as any, body)
49
50 const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' }
51 const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
52
53 expect(result).to.be.false
54 })
55
56 it('Should fail with an invalid PeerTube URL', async function () {
57 const keys = require('./json/peertube/keys.json')
58 const body = require('./json/peertube/announce-without-context.json')
59
60 const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
61 const signedBody = await buildSignedActivity(actorSignature as any, body)
62
63 const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9003/accounts/peertube' }
64 const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
65
66 expect(result).to.be.false
67 })
68
69 it('Should succeed with a valid PeerTube signature', async function () {
70 const keys = require('./json/peertube/keys.json')
71 const body = require('./json/peertube/announce-without-context.json')
72
73 const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
74 const signedBody = await buildSignedActivity(actorSignature as any, body)
75
76 const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' }
77 const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
78
79 expect(result).to.be.true
80 })
81 })
82
83 describe('When checking HTTP signature', function () {
84 it('Should fail with an invalid http signature', async function () {
85 const req = buildRequestStub()
86 req.method = 'POST'
87 req.url = '/accounts/ronan/inbox'
88
89 const mastodonObject = cloneDeep(require('./json/mastodon/bad-http-signature.json'))
90 req.body = mastodonObject.body
91 req.headers = mastodonObject.headers
92 req.headers.signature = 'Signature ' + req.headers.signature
93
94 const parsed = parseHTTPSignature(req, 3600 * 365 * 3)
95 const publicKey = require('./json/mastodon/public-key.json').publicKey
96
97 const actor = { publicKey }
98 const verified = isHTTPSignatureVerified(parsed, actor as any)
99
100 expect(verified).to.be.false
101 })
102
103 it('Should fail with an invalid public key', async function () {
104 const req = buildRequestStub()
105 req.method = 'POST'
106 req.url = '/accounts/ronan/inbox'
107
108 const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json'))
109 req.body = mastodonObject.body
110 req.headers = mastodonObject.headers
111 req.headers.signature = 'Signature ' + req.headers.signature
112
113 const parsed = parseHTTPSignature(req, 3600 * 365 * 3)
114 const publicKey = require('./json/mastodon/bad-public-key.json').publicKey
115
116 const actor = { publicKey }
117 const verified = isHTTPSignatureVerified(parsed, actor as any)
118
119 expect(verified).to.be.false
120 })
121
122 it('Should fail because of clock skew', async function () {
123 const req = buildRequestStub()
124 req.method = 'POST'
125 req.url = '/accounts/ronan/inbox'
126
127 const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json'))
128 req.body = mastodonObject.body
129 req.headers = mastodonObject.headers
130 req.headers.signature = 'Signature ' + req.headers.signature
131
132 let errored = false
133 try {
134 parseHTTPSignature(req)
135 } catch {
136 errored = true
137 }
138
139 expect(errored).to.be.true
140 })
141
142 it('Should fail without scheme', async function () {
143 const req = buildRequestStub()
144 req.method = 'POST'
145 req.url = '/accounts/ronan/inbox'
146
147 const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json'))
148 req.body = mastodonObject.body
149 req.headers = mastodonObject.headers
150
151 let errored = false
152 try {
153 parseHTTPSignature(req, 3600 * 365 * 3)
154 } catch {
155 errored = true
156 }
157
158 expect(errored).to.be.true
159 })
160
161 it('Should succeed with a valid signature', async function () {
162 const req = buildRequestStub()
163 req.method = 'POST'
164 req.url = '/accounts/ronan/inbox'
165
166 const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json'))
167 req.body = mastodonObject.body
168 req.headers = mastodonObject.headers
169 req.headers.signature = 'Signature ' + req.headers.signature
170
171 const parsed = parseHTTPSignature(req, 3600 * 365 * 3)
172 const publicKey = require('./json/mastodon/public-key.json').publicKey
173
174 const actor = { publicKey }
175 const verified = isHTTPSignatureVerified(parsed, actor as any)
176
177 expect(verified).to.be.true
178 })
179
180 })
181
182})
diff --git a/server/tests/api/activitypub/index.ts b/server/tests/api/activitypub/index.ts
new file mode 100644
index 000000000..e748f32e9
--- /dev/null
+++ b/server/tests/api/activitypub/index.ts
@@ -0,0 +1,4 @@
1import './client'
2import './fetch'
3import './helpers'
4import './security'
diff --git a/server/tests/api/activitypub/json/mastodon/bad-body-http-signature.json b/server/tests/api/activitypub/json/mastodon/bad-body-http-signature.json
new file mode 100644
index 000000000..4e7bc3af5
--- /dev/null
+++ b/server/tests/api/activitypub/json/mastodon/bad-body-http-signature.json
@@ -0,0 +1,93 @@
1{
2 "headers": {
3 "user-agent": "http.rb/3.3.0 (Mastodon/2.5.0; +http://localhost:3000/)",
4 "host": "localhost",
5 "date": "Mon, 22 Oct 2018 13:34:22 GMT",
6 "accept-encoding": "gzip",
7 "digest": "SHA-256=FEr5j2WSSfdEMcG3NTOXuGU0lUchfTJx4+BtUlWOwDk=",
8 "content-type": "application/activity+json",
9 "signature": "keyId=\"http://localhost:3000/users/ronan2#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"oLKbgxdFXdXsHJ3x/UsG9Svu7oa8Dyqiy6Jif4wqNuhAqRVMRaG18f+dd2OcfFX3XRGF8p8flZkU6vvoEQBauTwGRGcgXAJuKC1zYIWGk+PeiW8lNUnE4qGapWcTiFnIo7FKauNdsgqg/tvgs1pQIdHkDDjZMI64twP7sTN/4vG1PCq+kyqi/DM+ORLi/W7vFuLVHt2Iz7ikfw/R3/mMtS4FwLops+tVYBQ2iQ9DVRhTwLKVbeL/LLVB/tdGzNZ4F4nImBAQQ9I7WpPM6J/k+cBmoEbrUKs8ptx9gbX3OSsl5wlvPVMNzU9F9yb2MrB/Y/J4qssKz+LbiaktKGj7OQ==\"",
10 "content-length": "2815"
11 },
12 "body": {
13 "@context": [
14 "https://www.w3.org/ns/activitystreams",
15 "https://w3id.org/security/v1",
16 {
17 "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
18 "sensitive": "as:sensitive",
19 "movedTo": {
20 "@id": "as:movedTo",
21 "@type": "@id"
22 },
23 "Hashtag": "as:Hashtag",
24 "ostatus": "http://ostatus.org#",
25 "atomUri": "ostatus:atomUri",
26 "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
27 "conversation": "ostatus:conversation",
28 "toot": "http://joinmastodon.org/ns#",
29 "Emoji": "toot:Emoji",
30 "focalPoint": {
31 "@container": "@list",
32 "@id": "toot:focalPoint"
33 },
34 "featured": {
35 "@id": "toot:featured",
36 "@type": "@id"
37 },
38 "schema": "http://schema.org#",
39 "PropertyValue": "schema:PropertyValue",
40 "value": "schema:value"
41 }
42 ],
43 "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948/activity",
44 "type": "Create",
45 "actor": "http://localhost:3000/users/ronan2",
46 "published": "2018-10-22T13:34:18Z",
47 "to": [
48 "https://www.w3.org/ns/activitystreams#Public"
49 ],
50 "cc": [
51 "http://localhost:3000/users/ronan2/followers",
52 "http://localhost:9000/accounts/ronan"
53 ],
54 "object": {
55 "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
56 "type": "Note",
57 "summary": null,
58 "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
59 "published": "2018-10-22T13:34:18Z",
60 "url": "http://localhost:3000/@ronan2/100939547203370948",
61 "attributedTo": "http://localhost:3000/users/ronan2",
62 "to": [
63 "https://www.w3.org/ns/activitystreams#Public"
64 ],
65 "cc": [
66 "http://localhost:3000/users/ronan2/followers",
67 "http://localhost:9000/accounts/ronan"
68 ],
69 "sensitive": false,
70 "atomUri": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
71 "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
72 "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
73 "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>",
74 "contentMap": {
75 "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>"
76 },
77 "attachment": [],
78 "tag": [
79 {
80 "type": "Mention",
81 "href": "http://localhost:9000/accounts/ronan",
82 "name": "@ronan@localhost:9000"
83 }
84 ]
85 },
86 "signature": {
87 "type": "RsaSignature2017",
88 "creator": "http://localhost:3000/users/ronan2#main-key",
89 "created": "2018-10-22T13:34:19Z",
90 "signatureValue": "x+xL4l8ERziYVhwEafHJyBQOInvNZ0gV4ccYd9AtFYeGJagc8fY6jjjhbDRCD7yMhgTjBX69z20MXnDuwpmM6wej3dt1wLKdIyXVViO84nAlqFz7KmNxtk5lDnAVX/vttscT5YUFvw4dbPT2mQiEd1lKbaLftRiIPEomZpQ37+fUkQdcPrnhruPAISO/Sof1n1LFW4mYIffozteQSZBH6HaCVp+MRMIhdMi5e8w7PD48/cZz8D/EU8Vqi91FM76/3tMqg6nLqQ+8bq74Jvt2kzwZlIufe+I55QMpZOmF6hGIJEt+R0JXdjQbtgcELONmNj2dr8sAlzu7zKlAGuJ24Q=="
91 }
92 }
93}
diff --git a/server/tests/api/activitypub/json/mastodon/bad-http-signature.json b/server/tests/api/activitypub/json/mastodon/bad-http-signature.json
new file mode 100644
index 000000000..098597db0
--- /dev/null
+++ b/server/tests/api/activitypub/json/mastodon/bad-http-signature.json
@@ -0,0 +1,93 @@
1{
2 "headers": {
3 "user-agent": "http.rb/3.3.0 (Mastodon/2.5.0; +http://localhost:3000/)",
4 "host": "localhost",
5 "date": "Mon, 22 Oct 2018 13:34:22 GMT",
6 "accept-encoding": "gzip",
7 "digest": "SHA-256=FEr5j2WSSfdEMcG3NTOXuGU0lUchfTJx4+BtUlWOwDk=",
8 "content-type": "application/activity+json",
9 "signature": "keyId=\"http://localhost:3000/users/ronan2#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"oLKbgxdFXdXsHJ3x/UsG9Svu7oa8Dyqiy6Jif4wqNuhAqRVMRaG18f+dd2OcfFX3XRGF8p8flZkU6vvoEQBauTwGRGcgXAJuKC1zYIWGk+PeiW8lNUnE4qGapWcTiFnIo7FKauNdsgqg/tvgs1pQIdHkDDjZMI64twP7sTN/4vG1PCq+kyqi/DM+ORLi/W7vFuLVHt2Iz7ikfw/R3/mMtS4FwLops+tVYBQ2iQ9DVRhTwLKVbeL/LLVB/tdGzNZ4F4nImBAQQ9I7WpPM6J/k+cBmoEbrUKs8ptx9gbX3OSsl4wlvPVMNzU9F9yb2MrB/Y/J4qssKz+LbiaktKGj7OQ==\"",
10 "content-length": "2815"
11 },
12 "body": {
13 "@context": [
14 "https://www.w3.org/ns/activitystreams",
15 "https://w3id.org/security/v1",
16 {
17 "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
18 "sensitive": "as:sensitive",
19 "movedTo": {
20 "@id": "as:movedTo",
21 "@type": "@id"
22 },
23 "Hashtag": "as:Hashtag",
24 "ostatus": "http://ostatus.org#",
25 "atomUri": "ostatus:atomUri",
26 "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
27 "conversation": "ostatus:conversation",
28 "toot": "http://joinmastodon.org/ns#",
29 "Emoji": "toot:Emoji",
30 "focalPoint": {
31 "@container": "@list",
32 "@id": "toot:focalPoint"
33 },
34 "featured": {
35 "@id": "toot:featured",
36 "@type": "@id"
37 },
38 "schema": "http://schema.org#",
39 "PropertyValue": "schema:PropertyValue",
40 "value": "schema:value"
41 }
42 ],
43 "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948/activity",
44 "type": "Create",
45 "actor": "http://localhost:3000/users/ronan2",
46 "published": "2018-10-22T13:34:18Z",
47 "to": [
48 "https://www.w3.org/ns/activitystreams#Public"
49 ],
50 "cc": [
51 "http://localhost:3000/users/ronan2/followers",
52 "http://localhost:9000/accounts/ronan"
53 ],
54 "object": {
55 "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
56 "type": "Note",
57 "summary": null,
58 "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
59 "published": "2018-10-22T13:34:18Z",
60 "url": "http://localhost:3000/@ronan2/100939547203370948",
61 "attributedTo": "http://localhost:3000/users/ronan2",
62 "to": [
63 "https://www.w3.org/ns/activitystreams#Public"
64 ],
65 "cc": [
66 "http://localhost:3000/users/ronan2/followers",
67 "http://localhost:9000/accounts/ronan"
68 ],
69 "sensitive": false,
70 "atomUri": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
71 "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
72 "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
73 "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>",
74 "contentMap": {
75 "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>"
76 },
77 "attachment": [],
78 "tag": [
79 {
80 "type": "Mention",
81 "href": "http://localhost:9000/accounts/ronan",
82 "name": "@ronan@localhost:9000"
83 }
84 ]
85 },
86 "signature": {
87 "type": "RsaSignature2017",
88 "creator": "http://localhost:3000/users/ronan2#main-key",
89 "created": "2018-10-22T13:34:19Z",
90 "signatureValue": "x+xL4l8ERziYVhwEafHJyBQOInvNZ0gV4ccYd9AtFYeGJagc8fY6jjjhbDRCD7yMhgTjBX69z20MXnDuwpmM6wej3dt1wLKdIyXVViO84nAlqFz7KmNxtk5lDnAVX/vttscT5YUFvw4dbPT2mQiEd1lKbaLftRiIPEomZpQ37+fUkQdcPrnhruPAISO/Sof1n1LFW4mYIffozteQSZBH6HaCVp+MRMIhdMi5e8w7PD48/cZz8D/EU8Vqi91FM76/3tMqg6nLqQ+8bq74Jvt2kzwZlIufe+I55QMpZOmF6hGIJEt+R0JXdjQbtgcELONmNj2dr8sAlzu7zKlAGuJ24Q=="
91 }
92 }
93}
diff --git a/server/tests/api/activitypub/json/mastodon/bad-public-key.json b/server/tests/api/activitypub/json/mastodon/bad-public-key.json
new file mode 100644
index 000000000..73d18b3ad
--- /dev/null
+++ b/server/tests/api/activitypub/json/mastodon/bad-public-key.json
@@ -0,0 +1,3 @@
1{
2 "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0YyuthHtWWgDe0Fdgdp2\ndC5dTJsRqW6pFw5omIYYYjoES/WRewhVxEA54BhmxD3L1zChfx131N1TS8jVowhW\nm999jpUffKCCvLgYKIXETJDHiDeMONVx8wp7v9fS1HiFXo/E5und39gUMs14CMFZ\n6PE5jRV3r4XIKQJHQl7/X5n5FOb2934K+1TKUeBkbft/AushlKatYQakt3qHxpwx\nFvE+JjGo7QTnzdjaOx/e5QvojdGi2Kx4+jl77j2WVcSo5lOBz04OAVJtChtn82vS\nulPdDh3hZcDn+WK67yAhGP6AnzvOybZZS4zowlKiQ3kqjVVXKdl8gAsL4Y7MZ40R\nJQIDAQAB\n-----END PUBLIC KEY-----\n"
3}
diff --git a/server/tests/api/activitypub/json/mastodon/create-bad-signature.json b/server/tests/api/activitypub/json/mastodon/create-bad-signature.json
new file mode 100644
index 000000000..2cd037241
--- /dev/null
+++ b/server/tests/api/activitypub/json/mastodon/create-bad-signature.json
@@ -0,0 +1,81 @@
1{
2 "@context": [
3 "https://www.w3.org/ns/activitystreams",
4 "https://w3id.org/security/v1",
5 {
6 "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
7 "sensitive": "as:sensitive",
8 "movedTo": {
9 "@id": "as:movedTo",
10 "@type": "@id"
11 },
12 "Hashtag": "as:Hashtag",
13 "ostatus": "http://ostatus.org#",
14 "atomUri": "ostatus:atomUri",
15 "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
16 "conversation": "ostatus:conversation",
17 "toot": "http://joinmastodon.org/ns#",
18 "Emoji": "toot:Emoji",
19 "focalPoint": {
20 "@container": "@list",
21 "@id": "toot:focalPoint"
22 },
23 "featured": {
24 "@id": "toot:featured",
25 "@type": "@id"
26 },
27 "schema": "http://schema.org#",
28 "PropertyValue": "schema:PropertyValue",
29 "value": "schema:value"
30 }
31 ],
32 "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698/activity",
33 "type": "Create",
34 "actor": "http://localhost:3000/users/ronan2",
35 "published": "2018-10-22T12:43:07Z",
36 "to": [
37 "https://www.w3.org/ns/activitystreams#Public"
38 ],
39 "cc": [
40 "http://localhost:3000/users/ronan2/followers",
41 "http://localhost:9000/accounts/ronan"
42 ],
43 "object": {
44 "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698",
45 "type": "Note",
46 "summary": null,
47 "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
48 "published": "2018-10-22T12:43:07Z",
49 "url": "http://localhost:3000/@ronan2/100939345950887698",
50 "attributedTo": "http://localhost:3000/users/ronan2",
51 "to": [
52 "https://www.w3.org/ns/activitystreams#Public"
53 ],
54 "cc": [
55 "http://localhost:3000/users/ronan2/followers",
56 "http://localhost:9000/accounts/ronan"
57 ],
58 "sensitive": false,
59 "atomUri": "http://localhost:3000/users/ronan2/statuses/100939345950887698",
60 "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
61 "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
62 "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>",
63 "contentMap": {
64 "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>"
65 },
66 "attachment": [],
67 "tag": [
68 {
69 "type": "Mention",
70 "href": "http://localhost:9000/accounts/ronan",
71 "name": "@ronan@localhost:9000"
72 }
73 ]
74 },
75 "signature": {
76 "type": "RsaSignature2017",
77 "creator": "http://localhost:3000/users/ronan2#main-key",
78 "created": "2018-10-22T12:43:08Z",
79 "signatureValue": "Vgr8nA0agPr9TcA4BlX+MWhmuE+rBcoIJLpnPbm3E5SnOCXbgjEfEaTLqfuzzkKNsR3PBbkvi3YWK4/DxJ0zmpzSB7yy4NRzluQMVQHqJiFKXAX3Sr3fIrK24xkWW9/F207c1NpFajSGbgnFKBdtFE0e5VqwSrSoOJkZukZW/2ATSnsyzblieuUmvTWpD0PqpUOsynPjw+RqZnqPn0cjw1z2Dm7ZRt3trnyMTXFYZw5U/YuqMY2kpadD6vq780md8kXlJIylxG6ZrlO2jz9fJdnfuVq43d4QFNsBm1K1r2WtNqX+i+wiqh+u3PjF4pzXtl/a3hJOH18IfZnK7I21mQ=="
80 }
81}
diff --git a/server/tests/api/activitypub/json/mastodon/create.json b/server/tests/api/activitypub/json/mastodon/create.json
new file mode 100644
index 000000000..0be271bb8
--- /dev/null
+++ b/server/tests/api/activitypub/json/mastodon/create.json
@@ -0,0 +1,81 @@
1{
2 "@context": [
3 "https://www.w3.org/ns/activitystreams",
4 "https://w3id.org/security/v1",
5 {
6 "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
7 "sensitive": "as:sensitive",
8 "movedTo": {
9 "@id": "as:movedTo",
10 "@type": "@id"
11 },
12 "Hashtag": "as:Hashtag",
13 "ostatus": "http://ostatus.org#",
14 "atomUri": "ostatus:atomUri",
15 "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
16 "conversation": "ostatus:conversation",
17 "toot": "http://joinmastodon.org/ns#",
18 "Emoji": "toot:Emoji",
19 "focalPoint": {
20 "@container": "@list",
21 "@id": "toot:focalPoint"
22 },
23 "featured": {
24 "@id": "toot:featured",
25 "@type": "@id"
26 },
27 "schema": "http://schema.org#",
28 "PropertyValue": "schema:PropertyValue",
29 "value": "schema:value"
30 }
31 ],
32 "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698/activity",
33 "type": "Create",
34 "actor": "http://localhost:3000/users/ronan2",
35 "published": "2018-10-22T12:43:07Z",
36 "to": [
37 "https://www.w3.org/ns/activitystreams#Public"
38 ],
39 "cc": [
40 "http://localhost:3000/users/ronan2/followers",
41 "http://localhost:9000/accounts/ronan"
42 ],
43 "object": {
44 "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698",
45 "type": "Note",
46 "summary": null,
47 "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
48 "published": "2018-10-22T12:43:07Z",
49 "url": "http://localhost:3000/@ronan2/100939345950887698",
50 "attributedTo": "http://localhost:3000/users/ronan2",
51 "to": [
52 "https://www.w3.org/ns/activitystreams#Public"
53 ],
54 "cc": [
55 "http://localhost:3000/users/ronan2/followers",
56 "http://localhost:9000/accounts/ronan"
57 ],
58 "sensitive": false,
59 "atomUri": "http://localhost:3000/users/ronan2/statuses/100939345950887698",
60 "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
61 "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
62 "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>",
63 "contentMap": {
64 "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>"
65 },
66 "attachment": [],
67 "tag": [
68 {
69 "type": "Mention",
70 "href": "http://localhost:9000/accounts/ronan",
71 "name": "@ronan@localhost:9000"
72 }
73 ]
74 },
75 "signature": {
76 "type": "RsaSignature2017",
77 "creator": "http://localhost:3000/users/ronan2#main-key",
78 "created": "2018-10-22T12:43:08Z",
79 "signatureValue": "VgR8nA0agPr9TcA4BlX+MWhmuE+rBcoIJLpnPbm3E5SnOCXbgjEfEaTLqfuzzkKNsR3PBbkvi3YWK4/DxJ0zmpzSB7yy4NRzluQMVQHqJiFKXAX3Sr3fIrK24xkWW9/F207c1NpFajSGbgnFKBdtFE0e5VqwSrSoOJkZukZW/2ATSnsyzblieuUmvTWpD0PqpUOsynPjw+RqZnqPn0cjw1z2Dm7ZRt3trnyMTXFYZw5U/YuqMY2kpadD6vq780md8kXlJIylxG6ZrlO2jz9fJdnfuVq43d4QFNsBm1K1r2WtNqX+i+wiqh+u3PjF4pzXtl/a3hJOH18IfZnK7I21mQ=="
80 }
81}
diff --git a/server/tests/api/activitypub/json/mastodon/http-signature.json b/server/tests/api/activitypub/json/mastodon/http-signature.json
new file mode 100644
index 000000000..4e7bc3af5
--- /dev/null
+++ b/server/tests/api/activitypub/json/mastodon/http-signature.json
@@ -0,0 +1,93 @@
1{
2 "headers": {
3 "user-agent": "http.rb/3.3.0 (Mastodon/2.5.0; +http://localhost:3000/)",
4 "host": "localhost",
5 "date": "Mon, 22 Oct 2018 13:34:22 GMT",
6 "accept-encoding": "gzip",
7 "digest": "SHA-256=FEr5j2WSSfdEMcG3NTOXuGU0lUchfTJx4+BtUlWOwDk=",
8 "content-type": "application/activity+json",
9 "signature": "keyId=\"http://localhost:3000/users/ronan2#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"oLKbgxdFXdXsHJ3x/UsG9Svu7oa8Dyqiy6Jif4wqNuhAqRVMRaG18f+dd2OcfFX3XRGF8p8flZkU6vvoEQBauTwGRGcgXAJuKC1zYIWGk+PeiW8lNUnE4qGapWcTiFnIo7FKauNdsgqg/tvgs1pQIdHkDDjZMI64twP7sTN/4vG1PCq+kyqi/DM+ORLi/W7vFuLVHt2Iz7ikfw/R3/mMtS4FwLops+tVYBQ2iQ9DVRhTwLKVbeL/LLVB/tdGzNZ4F4nImBAQQ9I7WpPM6J/k+cBmoEbrUKs8ptx9gbX3OSsl5wlvPVMNzU9F9yb2MrB/Y/J4qssKz+LbiaktKGj7OQ==\"",
10 "content-length": "2815"
11 },
12 "body": {
13 "@context": [
14 "https://www.w3.org/ns/activitystreams",
15 "https://w3id.org/security/v1",
16 {
17 "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
18 "sensitive": "as:sensitive",
19 "movedTo": {
20 "@id": "as:movedTo",
21 "@type": "@id"
22 },
23 "Hashtag": "as:Hashtag",
24 "ostatus": "http://ostatus.org#",
25 "atomUri": "ostatus:atomUri",
26 "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
27 "conversation": "ostatus:conversation",
28 "toot": "http://joinmastodon.org/ns#",
29 "Emoji": "toot:Emoji",
30 "focalPoint": {
31 "@container": "@list",
32 "@id": "toot:focalPoint"
33 },
34 "featured": {
35 "@id": "toot:featured",
36 "@type": "@id"
37 },
38 "schema": "http://schema.org#",
39 "PropertyValue": "schema:PropertyValue",
40 "value": "schema:value"
41 }
42 ],
43 "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948/activity",
44 "type": "Create",
45 "actor": "http://localhost:3000/users/ronan2",
46 "published": "2018-10-22T13:34:18Z",
47 "to": [
48 "https://www.w3.org/ns/activitystreams#Public"
49 ],
50 "cc": [
51 "http://localhost:3000/users/ronan2/followers",
52 "http://localhost:9000/accounts/ronan"
53 ],
54 "object": {
55 "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
56 "type": "Note",
57 "summary": null,
58 "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
59 "published": "2018-10-22T13:34:18Z",
60 "url": "http://localhost:3000/@ronan2/100939547203370948",
61 "attributedTo": "http://localhost:3000/users/ronan2",
62 "to": [
63 "https://www.w3.org/ns/activitystreams#Public"
64 ],
65 "cc": [
66 "http://localhost:3000/users/ronan2/followers",
67 "http://localhost:9000/accounts/ronan"
68 ],
69 "sensitive": false,
70 "atomUri": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
71 "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
72 "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
73 "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>",
74 "contentMap": {
75 "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>"
76 },
77 "attachment": [],
78 "tag": [
79 {
80 "type": "Mention",
81 "href": "http://localhost:9000/accounts/ronan",
82 "name": "@ronan@localhost:9000"
83 }
84 ]
85 },
86 "signature": {
87 "type": "RsaSignature2017",
88 "creator": "http://localhost:3000/users/ronan2#main-key",
89 "created": "2018-10-22T13:34:19Z",
90 "signatureValue": "x+xL4l8ERziYVhwEafHJyBQOInvNZ0gV4ccYd9AtFYeGJagc8fY6jjjhbDRCD7yMhgTjBX69z20MXnDuwpmM6wej3dt1wLKdIyXVViO84nAlqFz7KmNxtk5lDnAVX/vttscT5YUFvw4dbPT2mQiEd1lKbaLftRiIPEomZpQ37+fUkQdcPrnhruPAISO/Sof1n1LFW4mYIffozteQSZBH6HaCVp+MRMIhdMi5e8w7PD48/cZz8D/EU8Vqi91FM76/3tMqg6nLqQ+8bq74Jvt2kzwZlIufe+I55QMpZOmF6hGIJEt+R0JXdjQbtgcELONmNj2dr8sAlzu7zKlAGuJ24Q=="
91 }
92 }
93}
diff --git a/server/tests/api/activitypub/json/mastodon/public-key.json b/server/tests/api/activitypub/json/mastodon/public-key.json
new file mode 100644
index 000000000..b7b9b8308
--- /dev/null
+++ b/server/tests/api/activitypub/json/mastodon/public-key.json
@@ -0,0 +1,3 @@
1{
2 "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0YyuthHtWWgDe0Fdgdp2\ndC5dTJsRqW6pFw5omIYYYjoES/WRewhVxEA54BhmxD3L1zChfx131N1TS8jVowhW\nm999jpUffKCCvLgYKIXETJDHiDeMONVx8wp7v9fS1HiFXo/E5und39gUMs14CMFZ\n6PE5jRV3r4XIKQJHQl7/X5n5FOb2934K+1TKUeBkbft/AushlKatYQakt3qHxpwx\nFvE+JjGo7QTnzdjaOx/e5QvojdGi2Kx4+jl87j2WVcSo5lOBz04OAVJtChtn82vS\nulPdDh3hZcDn+WK67yAhGP6AnzvOybZZS4zowlKiQ3kqjVVXKdl8gAsL4Y7MZ40R\nJQIDAQAB\n-----END PUBLIC KEY-----\n"
3}
diff --git a/server/tests/api/activitypub/json/peertube/announce-without-context.json b/server/tests/api/activitypub/json/peertube/announce-without-context.json
new file mode 100644
index 000000000..5f2af0cde
--- /dev/null
+++ b/server/tests/api/activitypub/json/peertube/announce-without-context.json
@@ -0,0 +1,13 @@
1{
2 "type": "Announce",
3 "id": "http://localhost:9002/videos/watch/997111d4-e8d8-4f45-99d3-857905785d05/announces/1",
4 "actor": "http://localhost:9002/accounts/peertube",
5 "object": "http://localhost:9002/videos/watch/997111d4-e8d8-4f45-99d3-857905785d05",
6 "to": [
7 "https://www.w3.org/ns/activitystreams#Public",
8 "http://localhost:9002/accounts/peertube/followers",
9 "http://localhost:9002/video-channels/root_channel/followers",
10 "http://localhost:9002/accounts/root/followers"
11 ],
12 "cc": []
13}
diff --git a/server/tests/api/activitypub/json/peertube/invalid-keys.json b/server/tests/api/activitypub/json/peertube/invalid-keys.json
new file mode 100644
index 000000000..0544e96b9
--- /dev/null
+++ b/server/tests/api/activitypub/json/peertube/invalid-keys.json
@@ -0,0 +1,6 @@
1{
2 "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqjQGdH6D3naKmSbbr/Df\nEh1H42F3WlHYXuxKLkm5Bemjdde+GwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYO\nwAyc3Zoy7afPNa4bZXqhJ1Im41rMGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55s\nIkczDkseJuadTvG+A1e4uNY2lnRmVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/F\npP5S75TS5l1DfJQIq2lp8RwrH6FvGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM\n7mS7eP8zF8lKXYUu8cjIscKm+XqGmyRoPyw2Pp53tew29idRUocVQHGBnlNbpKdd\naQIDAQAB\n-----END PUBLIC KEY-----\n",
3 "privateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAqjQGdH6D3naKmSbbr/DfEh1H42F3WlHYXuxKLkm5Bemjdde+\nGwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYOwAyc3Zoy7afPNa4bZXqhJ1Im41rM\nGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55sIkczDkseJuadTvG+A1e4uNY2lnRm\nVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/FpP5S75TS5l1DfJQIq2lp8RwrH6Fv\nGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM7mS7eP8zF8lKXYUu8cjIscKm+XqG\nmyRoPyw3Pp53tew29idRUocVQHGBnlNbpKddaQIDAQABAoIBAQCnBZawCtbtH/ay\ng+dhqEW/SOyavbKZ92cU/1tsQPxISRYXNjdf2VfK7HmVqC2S7NqBanz+AVZPHmda\n7OfamkSvQbFN5VvEy8ATNV+9HbG3HG78/MT9hZcGigmyJkcZuy4wILgoXCxfpxlD\netla60PB/4yioiRcmEIWjjOgpByphDJ7RuuuptyEvgjUjpPtvHK47O/loaD2HFJk\nbIYbRirbjUjITRjQxGVIvanqiwPG9pB26YDLxDOoXEumcnzRcEFWNdvoleaLgquS\nn/zVsXWEq4+1i7t44DDstWUt/2Bw5ksIkSdayQ6oy3vzre3YFHwvbVZ7qtQQgpru\nx+NIolZhAoGBAN1RgNj8zy9Py3SJdsoXtnuCItfD7eo7LWXUa06cM/NS695Q+/to\naa5i3cJnRlv+b+b3VvnhkhIBLfFQW+hWwPnnxJEehcm09ddN9zbWrZ4Yv9yYu+8d\nTLGyWL8kPFF1dz+29DcrSv3tXEOwxByX/O4U/X/i3wl2WhkybxVFnCuvAoGBAMTf\n91BgLzvcYKOxH+vRPOJY7g2HKGFe35R91M4E+9Eq1rq4LUQHBb3fhRh4+scNu0yb\nNfN1Zdx2nbgCXdTKomF1Ahxp58/A2iU65vVzL6hYfWXEGSmoBqsGCIpIxQ9jgB9k\nCl7t/Ban8Z/ORHTjI9fpHlSZyCWJ3ajepiM2a1ZnAoGAPpDO6wi1DXvyWVSPF1yS\nwuGsNfD2rjPihpoBZ+yypwP3GBcu1QjUb28Vn+KQOmt4eQPNO8DwCVT6BvEfulPk\nJAHISPom+jnFEgPBcmhIFpyKiLNI1bUjvExd2FNHFgQuHP38ligQAC782Un8dtTk\ntO2MKH4bbVJe8CaYzpuqJZMCgYABZyMpBHZxs8FQiUuT75rCdiXEHOlxwC5RrY/d\no/VzaR28mOFhsbcdwkD9iqcm0fc6tYRt5rFCH+pBzGqEwKjljuLj9vE67sHfMAtD\nRn3Zcj/6gKo5PMRHZbSb36bf1DKuhpT4VjPMqYe0PtEIEDJKMJQRwELH2bKlqGiA\nqbucEwKBgQCkS85JnpHEV/tSylsEEn2W3CQCx58zl7iZNV7h/tWMR4AyrcI0HqP6\nllJ7V/Cfw66MgelPnosKgagwLVI6gsqDtjnzYo3XuMRVlYIySJ/jV3eiUNkV2Ky2\nfp/gA9sVgp38QSr+xB9E0LNStcbqDzoCCcDRws/SK7PbkQH9KV47tQ==\n-----END RSA PRIVATE KEY-----"
4}
5
6
diff --git a/server/tests/api/activitypub/json/peertube/keys.json b/server/tests/api/activitypub/json/peertube/keys.json
new file mode 100644
index 000000000..1a7700865
--- /dev/null
+++ b/server/tests/api/activitypub/json/peertube/keys.json
@@ -0,0 +1,4 @@
1{
2 "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqjQGdH6D3naKmSbbr/Df\nEh1H42F3WlHYXuxKLkm5Bemjdde+GwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYO\nwAyc3Zoy7afPNa4bZXqhJ1Im41rMGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55s\nIkczDkseJuadTvG+A1e4uNY2lnRmVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/F\npP5S75TS5l1DfJQIq2lp8RwrH6FvGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM\n7mS7eP8zF8lKXYUu8cjIscKm+XqGmyRoPyw3Pp53tew29idRUocVQHGBnlNbpKdd\naQIDAQAB\n-----END PUBLIC KEY-----\n",
3 "privateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAqjQGdH6D3naKmSbbr/DfEh1H42F3WlHYXuxKLkm5Bemjdde+\nGwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYOwAyc3Zoy7afPNa4bZXqhJ1Im41rM\nGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55sIkczDkseJuadTvG+A1e4uNY2lnRm\nVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/FpP5S75TS5l1DfJQIq2lp8RwrH6Fv\nGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM7mS7eP8zF8lKXYUu8cjIscKm+XqG\nmyRoPyw3Pp53tew29idRUocVQHGBnlNbpKddaQIDAQABAoIBAQCnBZawCtbtH/ay\ng+dhqEW/SOyavbKZ92cU/1tsQPxISRYXNjdf2VfK7HmVqC2S7NqBanz+AVZPHmda\n7OfamkSvQbFN5VvEy8ATNV+9HbG3HG78/MT9hZcGigmyJkcZuy4wILgoXCxfpxlD\netla60PB/4yioiRcmEIWjjOgpByphDJ7RuuuptyEvgjUjpPtvHK47O/loaD2HFJk\nbIYbRirbjUjITRjQxGVIvanqiwPG9pB26YDLxDOoXEumcnzRcEFWNdvoleaLgquS\nn/zVsXWEq4+1i7t44DDstWUt/2Bw5ksIkSdayQ6oy3vzre3YFHwvbVZ7qtQQgpru\nx+NIolZhAoGBAN1RgNj8zy9Py3SJdsoXtnuCItfD7eo7LWXUa06cM/NS695Q+/to\naa5i3cJnRlv+b+b3VvnhkhIBLfFQW+hWwPnnxJEehcm09ddN9zbWrZ4Yv9yYu+8d\nTLGyWL8kPFF1dz+29DcrSv3tXEOwxByX/O4U/X/i3wl2WhkybxVFnCuvAoGBAMTf\n91BgLzvcYKOxH+vRPOJY7g2HKGFe35R91M4E+9Eq1rq4LUQHBb3fhRh4+scNu0yb\nNfN1Zdx2nbgCXdTKomF1Ahxp58/A2iU65vVzL6hYfWXEGSmoBqsGCIpIxQ9jgB9k\nCl7t/Ban8Z/ORHTjI9fpHlSZyCWJ3ajepiM2a1ZnAoGAPpDO6wi1DXvyWVSPF1yS\nwuGsNfD2rjPihpoBZ+yypwP3GBcu1QjUb28Vn+KQOmt4eQPNO8DwCVT6BvEfulPk\nJAHISPom+jnFEgPBcmhIFpyKiLNI1bUjvExd2FNHFgQuHP38ligQAC782Un8dtTk\ntO2MKH4bbVJe8CaYzpuqJZMCgYABZyMpBHZxs8FQiUuT75rCdiXEHOlxwC5RrY/d\no/VzaR28mOFhsbcdwkD9iqcm0fc6tYRt5rFCH+pBzGqEwKjljuLj9vE67sHfMAtD\nRn3Zcj/6gKo5PMRHZbSb36bf1DKuhpT4VjPMqYe0PtEIEDJKMJQRwELH2bKlqGiA\nqbucEwKBgQCkS85JnpHEV/tSylsEEn2W3CQCx58zl7iZNV7h/tWMR4AyrcI0HqP6\nllJ7V/Cfw66MgelPnosKgagwLVI6gsqDtjnzYo3XuMRVlYIySJ/jV3eiUNkV2Ky2\nfp/gA9sVgp38QSr+xB9E0LNStcbqDzoCCcDRws/SK7PbkQH9KV47tQ==\n-----END RSA PRIVATE KEY-----"
4}
diff --git a/server/tests/api/activitypub/security.ts b/server/tests/api/activitypub/security.ts
new file mode 100644
index 000000000..e7899bb14
--- /dev/null
+++ b/server/tests/api/activitypub/security.ts
@@ -0,0 +1,180 @@
1/* tslint:disable:no-unused-expression */
2
3import 'mocha'
4
5import { flushAndRunMultipleServers, flushTests, killallServers, makePOSTAPRequest, makeFollowRequest, ServerInfo } from '../../utils'
6import { HTTP_SIGNATURE } from '../../../initializers'
7import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
8import * as chai from 'chai'
9import { setActorField } from '../../utils/miscs/sql'
10import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub'
11
12const expect = chai.expect
13
14function setKeysOfServer2 (serverNumber: number, publicKey: string, privateKey: string) {
15 return Promise.all([
16 setActorField(serverNumber, 'http://localhost:9002/accounts/peertube', 'publicKey', publicKey),
17 setActorField(serverNumber, 'http://localhost:9002/accounts/peertube', 'privateKey', privateKey)
18 ])
19}
20
21function setKeysOfServer3 (serverNumber: number, publicKey: string, privateKey: string) {
22 return Promise.all([
23 setActorField(serverNumber, 'http://localhost:9003/accounts/peertube', 'publicKey', publicKey),
24 setActorField(serverNumber, 'http://localhost:9003/accounts/peertube', 'privateKey', privateKey)
25 ])
26}
27
28describe('Test ActivityPub security', function () {
29 let servers: ServerInfo[]
30 let url: string
31
32 const keys = require('./json/peertube/keys.json')
33 const invalidKeys = require('./json/peertube/invalid-keys.json')
34 const baseHttpSignature = {
35 algorithm: HTTP_SIGNATURE.ALGORITHM,
36 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
37 keyId: 'acct:peertube@localhost:9002',
38 key: keys.privateKey,
39 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
40 }
41
42 // ---------------------------------------------------------------
43
44 before(async function () {
45 this.timeout(60000)
46
47 servers = await flushAndRunMultipleServers(3)
48
49 url = servers[0].url + '/inbox'
50
51 await setKeysOfServer2(1, keys.publicKey, keys.privateKey)
52
53 const to = { url: 'http://localhost:9001/accounts/peertube' }
54 const by = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
55 await makeFollowRequest(to, by)
56 })
57
58 describe('When checking HTTP signature', function () {
59
60 it('Should fail with an invalid digest', async function () {
61 const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
62 const headers = {
63 Digest: buildDigest({ hello: 'coucou' })
64 }
65
66 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
67
68 expect(response.statusCode).to.equal(403)
69 })
70
71 it('Should fail with an invalid date', async function () {
72 const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
73 const headers = buildGlobalHeaders(body)
74 headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
75
76 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
77
78 expect(response.statusCode).to.equal(403)
79 })
80
81 it('Should fail with bad keys', async function () {
82 await setKeysOfServer2(1, invalidKeys.publicKey, invalidKeys.privateKey)
83 await setKeysOfServer2(2, invalidKeys.publicKey, invalidKeys.privateKey)
84
85 const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
86 const headers = buildGlobalHeaders(body)
87
88 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
89
90 expect(response.statusCode).to.equal(403)
91 })
92
93 it('Should succeed with a valid HTTP signature', async function () {
94 await setKeysOfServer2(1, keys.publicKey, keys.privateKey)
95 await setKeysOfServer2(2, keys.publicKey, keys.privateKey)
96
97 const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
98 const headers = buildGlobalHeaders(body)
99
100 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
101
102 expect(response.statusCode).to.equal(204)
103 })
104 })
105
106 describe('When checking Linked Data Signature', function () {
107 before(async () => {
108 await setKeysOfServer3(3, keys.publicKey, keys.privateKey)
109
110 const to = { url: 'http://localhost:9001/accounts/peertube' }
111 const by = { url: 'http://localhost:9003/accounts/peertube', privateKey: keys.privateKey }
112 await makeFollowRequest(to, by)
113 })
114
115 it('Should fail with bad keys', async function () {
116 this.timeout(10000)
117
118 await setKeysOfServer3(1, invalidKeys.publicKey, invalidKeys.privateKey)
119 await setKeysOfServer3(3, invalidKeys.publicKey, invalidKeys.privateKey)
120
121 const body = require('./json/peertube/announce-without-context.json')
122 body.actor = 'http://localhost:9003/accounts/peertube'
123
124 const signer: any = { privateKey: invalidKeys.privateKey, url: 'http://localhost:9003/accounts/peertube' }
125 const signedBody = await buildSignedActivity(signer, body)
126
127 const headers = buildGlobalHeaders(signedBody)
128
129 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers)
130
131 expect(response.statusCode).to.equal(403)
132 })
133
134 it('Should fail with an altered body', async function () {
135 this.timeout(10000)
136
137 await setKeysOfServer3(1, keys.publicKey, keys.privateKey)
138 await setKeysOfServer3(3, keys.publicKey, keys.privateKey)
139
140 const body = require('./json/peertube/announce-without-context.json')
141 body.actor = 'http://localhost:9003/accounts/peertube'
142
143 const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:9003/accounts/peertube' }
144 const signedBody = await buildSignedActivity(signer, body)
145
146 signedBody.actor = 'http://localhost:9003/account/peertube'
147
148 const headers = buildGlobalHeaders(signedBody)
149
150 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers)
151
152 expect(response.statusCode).to.equal(403)
153 })
154
155 it('Should succeed with a valid signature', async function () {
156 this.timeout(10000)
157
158 const body = require('./json/peertube/announce-without-context.json')
159 body.actor = 'http://localhost:9003/accounts/peertube'
160
161 const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:9003/accounts/peertube' }
162 const signedBody = await buildSignedActivity(signer, body)
163
164 const headers = buildGlobalHeaders(signedBody)
165
166 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers)
167
168 expect(response.statusCode).to.equal(204)
169 })
170 })
171
172 after(async function () {
173 killallServers(servers)
174
175 // Keep the logs if the test failed
176 if (this['ok']) {
177 await flushTests()
178 }
179 })
180})
diff --git a/server/tests/api/index-4.ts b/server/tests/api/index-4.ts
index 8e69b95a6..7d8be2b3d 100644
--- a/server/tests/api/index-4.ts
+++ b/server/tests/api/index-4.ts
@@ -1 +1,2 @@
1import './redundancy' 1import './redundancy'
2import './activitypub'
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts
index 0af52023c..663e31ead 100644
--- a/server/tests/api/redundancy/redundancy.ts
+++ b/server/tests/api/redundancy/redundancy.ts
@@ -54,7 +54,7 @@ async function runServers (strategy: VideoRedundancyStrategy, additionalParams:
54 immutableAssign({ 54 immutableAssign({
55 min_lifetime: '1 hour', 55 min_lifetime: '1 hour',
56 strategy: strategy, 56 strategy: strategy,
57 size: '100KB' 57 size: '200KB'
58 }, additionalParams) 58 }, additionalParams)
59 ] 59 ]
60 } 60 }
@@ -111,8 +111,8 @@ async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) {
111 const stat = data.videosRedundancy[0] 111 const stat = data.videosRedundancy[0]
112 112
113 expect(stat.strategy).to.equal(strategy) 113 expect(stat.strategy).to.equal(strategy)
114 expect(stat.totalSize).to.equal(102400) 114 expect(stat.totalSize).to.equal(204800)
115 expect(stat.totalUsed).to.be.at.least(1).and.below(102401) 115 expect(stat.totalUsed).to.be.at.least(1).and.below(204801)
116 expect(stat.totalVideoFiles).to.equal(4) 116 expect(stat.totalVideoFiles).to.equal(4)
117 expect(stat.totalVideos).to.equal(1) 117 expect(stat.totalVideos).to.equal(1)
118} 118}
@@ -125,7 +125,7 @@ async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategy) {
125 125
126 const stat = data.videosRedundancy[0] 126 const stat = data.videosRedundancy[0]
127 expect(stat.strategy).to.equal(strategy) 127 expect(stat.strategy).to.equal(strategy)
128 expect(stat.totalSize).to.equal(102400) 128 expect(stat.totalSize).to.equal(204800)
129 expect(stat.totalUsed).to.equal(0) 129 expect(stat.totalUsed).to.equal(0)
130 expect(stat.totalVideoFiles).to.equal(0) 130 expect(stat.totalVideoFiles).to.equal(0)
131 expect(stat.totalVideos).to.equal(0) 131 expect(stat.totalVideos).to.equal(0)
@@ -223,7 +223,7 @@ describe('Test videos redundancy', function () {
223 return enableRedundancyOnServer1() 223 return enableRedundancyOnServer1()
224 }) 224 })
225 225
226 it('Should have 2 webseed on the first video', async function () { 226 it('Should have 2 webseeds on the first video', async function () {
227 this.timeout(40000) 227 this.timeout(40000)
228 228
229 await waitJobs(servers) 229 await waitJobs(servers)
@@ -270,7 +270,7 @@ describe('Test videos redundancy', function () {
270 return enableRedundancyOnServer1() 270 return enableRedundancyOnServer1()
271 }) 271 })
272 272
273 it('Should have 2 webseed on the first video', async function () { 273 it('Should have 2 webseeds on the first video', async function () {
274 this.timeout(40000) 274 this.timeout(40000)
275 275
276 await waitJobs(servers) 276 await waitJobs(servers)
@@ -338,7 +338,7 @@ describe('Test videos redundancy', function () {
338 await waitJobs(servers) 338 await waitJobs(servers)
339 }) 339 })
340 340
341 it('Should have 2 webseed on the first video', async function () { 341 it('Should have 2 webseeds on the first video', async function () {
342 this.timeout(40000) 342 this.timeout(40000)
343 343
344 await waitJobs(servers) 344 await waitJobs(servers)
@@ -419,7 +419,7 @@ describe('Test videos redundancy', function () {
419 419
420 killallServers([ servers[0] ]) 420 killallServers([ servers[0] ])
421 421
422 await wait(10000) 422 await wait(15000)
423 423
424 await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001') 424 await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001')
425 }) 425 })
@@ -451,27 +451,23 @@ describe('Test videos redundancy', function () {
451 video2Server2UUID = res.body.video.uuid 451 video2Server2UUID = res.body.video.uuid
452 }) 452 })
453 453
454 it('Should cache video 2 webseed on the first video', async function () { 454 it('Should cache video 2 webseeds on the first video', async function () {
455 this.timeout(50000) 455 this.timeout(120000)
456 456
457 await waitJobs(servers) 457 await waitJobs(servers)
458 458
459 await wait(7000) 459 let checked = false
460 460
461 try { 461 while (checked === false) {
462 await check1WebSeed(strategy, video1Server2UUID) 462 await wait(1000)
463 await check2Webseeds(strategy, video2Server2UUID)
464 } catch {
465 await wait(3000)
466 463
467 try { 464 try {
468 await check1WebSeed(strategy, video1Server2UUID) 465 await check1WebSeed(strategy, video1Server2UUID)
469 await check2Webseeds(strategy, video2Server2UUID) 466 await check2Webseeds(strategy, video2Server2UUID)
470 } catch {
471 await wait(5000)
472 467
473 await check1WebSeed(strategy, video1Server2UUID) 468 checked = true
474 await check2Webseeds(strategy, video2Server2UUID) 469 } catch {
470 checked = false
475 } 471 }
476 } 472 }
477 }) 473 })
diff --git a/server/tests/api/server/handle-down.ts b/server/tests/api/server/handle-down.ts
index e6e0d6c7a..8e162b69e 100644
--- a/server/tests/api/server/handle-down.ts
+++ b/server/tests/api/server/handle-down.ts
@@ -5,6 +5,7 @@ import 'mocha'
5import { JobState, Video } from '../../../../shared/models' 5import { JobState, Video } from '../../../../shared/models'
6import { VideoPrivacy } from '../../../../shared/models/videos' 6import { VideoPrivacy } from '../../../../shared/models/videos'
7import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' 7import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
8
8import { 9import {
9 completeVideoCheck, 10 completeVideoCheck,
10 getVideo, 11 getVideo,
@@ -18,6 +19,7 @@ import {
18 ServerInfo, 19 ServerInfo,
19 setAccessTokensToServers, 20 setAccessTokensToServers,
20 uploadVideo, 21 uploadVideo,
22 updateVideo,
21 wait 23 wait
22} from '../../../../shared/utils' 24} from '../../../../shared/utils'
23import { follow, getFollowersListPaginationAndSort } from '../../../../shared/utils/server/follows' 25import { follow, getFollowersListPaginationAndSort } from '../../../../shared/utils/server/follows'
@@ -199,15 +201,15 @@ describe('Test handle downs', function () {
199 expect(res.body.data).to.have.lengthOf(2) 201 expect(res.body.data).to.have.lengthOf(2)
200 }) 202 })
201 203
202 it('Should send a view to server 3, and automatically fetch the video', async function () { 204 it('Should send an update to server 3, and automatically fetch the video', async function () {
203 this.timeout(15000) 205 this.timeout(15000)
204 206
205 const res1 = await getVideosList(servers[2].url) 207 const res1 = await getVideosList(servers[2].url)
206 expect(res1.body.data).to.be.an('array') 208 expect(res1.body.data).to.be.an('array')
207 expect(res1.body.data).to.have.lengthOf(11) 209 expect(res1.body.data).to.have.lengthOf(11)
208 210
209 await viewVideo(servers[0].url, missedVideo1.uuid) 211 await updateVideo(servers[0].url, servers[0].accessToken, missedVideo1.uuid, { })
210 await viewVideo(servers[0].url, unlistedVideo.uuid) 212 await updateVideo(servers[0].url, servers[0].accessToken, unlistedVideo.uuid, { })
211 213
212 await waitJobs(servers) 214 await waitJobs(servers)
213 215
diff --git a/server/tests/api/server/index.ts b/server/tests/api/server/index.ts
index eeb8b7a28..78ab7e18b 100644
--- a/server/tests/api/server/index.ts
+++ b/server/tests/api/server/index.ts
@@ -6,3 +6,4 @@ import './jobs'
6import './reverse-proxy' 6import './reverse-proxy'
7import './stats' 7import './stats'
8import './tracker' 8import './tracker'
9import './no-client'
diff --git a/server/tests/api/server/no-client.ts b/server/tests/api/server/no-client.ts
new file mode 100644
index 000000000..6d6ce8532
--- /dev/null
+++ b/server/tests/api/server/no-client.ts
@@ -0,0 +1,36 @@
1import 'mocha'
2import * as request from 'supertest'
3import {
4 flushTests,
5 killallServers,
6 ServerInfo
7} from '../../utils/index'
8import { runServer } from '../../utils/server/servers'
9
10describe('Start and stop server without web client routes', function () {
11 let server: ServerInfo
12
13 before(async function () {
14 this.timeout(30000)
15
16 await flushTests()
17
18 server = await runServer(1, {}, ['--no-client'])
19 })
20
21 it('Should fail getting the client', function () {
22 const req = request(server.url)
23 .get('/')
24
25 return req.expect(404)
26 })
27
28 after(async function () {
29 killallServers([ server ])
30
31 // Keep the logs if the test failed
32 if (this['ok']) {
33 await flushTests()
34 }
35 })
36})